Text Animations

Liquid Reveal

Refracts children through an SVG displacement filter like a liquid droplet, then bursts the surface to reveal them crisply on click or in-view.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Wrap a logo, a headline or any content in a slowly morphing bead of liquid: the content wobbles as if seen through the surface while the texture frequency keeps "breathing". On trigger, the displacement surges to its limit, then springs back to zero; the bead flattens out and throws a ring of small droplets, leaving the content crisp. Good for brand intros, release headlines and hero reveals. The refraction is an inline SVG filter (feTurbulence + feDisplacementMap) — no canvas or WebGL.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/liquid-reveal.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

40
0.012
8
Active edited usageDefault props
<LiquidReveal/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/liquid-reveal.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/liquid-reveal.

Usage

import * as React from "react";
import {
  LiquidReveal,
  type LiquidRevealHandle,
} from "@/components/ui/liquid-reveal";

// Click to reveal: the container itself is a button
<LiquidReveal trigger="click" contentLabel="Shuihe wordmark" tint="rgba(56,189,248,0.16)">
  <Logo />
</LiquidReveal>;

// Reveal on entering the viewport, replay through ref
function Headline() {
  const ref = React.useRef<LiquidRevealHandle>(null);
  return (
    <>
      <LiquidReveal ref={ref} trigger="inView" droplets={10} grain>
        <h2 className="text-4xl font-bold">New release</h2>
      </LiquidReveal>
      <button type="button" onClick={() => ref.current?.reset()}>
        Replay
      </button>
    </>
  );
}

Props

PropTypeDefaultDescription
childrenReact.ReactNode—Content wrapped by the droplet and revealed at the end (text, SVG logos, icon groups)
revealedboolean—Controlled reveal state; managed internally when omitted
defaultRevealedbooleanfalseInitial reveal state in uncontrolled mode
onRevealedChange(revealed: boolean) => void—Called when the reveal state changes
trigger"click" | "inView" | "manual""inView"click turns the container into a button; inView bursts once 60% is in view; manual relies on revealed or ref only
restScalenumber40Idle displacement strength (feDisplacementMap scale); higher means stronger distortion
frequencynumber0.012Base frequency of the refraction texture (feTurbulence baseFrequency); higher means finer ripples
dropletsnumber8Number of small droplets thrown outward on burst (0 for none)
tintstring—Droplet tint (any CSS color); written to --wb-liquid-reveal-tint, transparent when unset
grainbooleanfalseOverlay fine grain on the surface for a tactile look
revealLabelstring"揭露內容" (Reveal content)Accessible name of the button before reveal when trigger="click"
contentLabelstring—Accessible name after reveal (content description); falls back to the children text when omitted
refReact.Ref<LiquidRevealHandle>—Exposes reveal() / reset()
classNamestring—Passed through to the outermost container

LiquidRevealHandle

FieldTypeDefaultDescription
reveal() => void—Reveal immediately (same as triggering one burst)
reset() => void—Return to the droplet state and resume the refraction wobble

LiquidRevealTrigger, LiquidRevealHandle and LiquidRevealProps are also exported as named types.

CSS variables

FieldTypeDefaultDescription
--wb-liquid-reveal-duration<time>700msHow long the bead flattens and the tint and highlight fade on reveal
--wb-liquid-reveal-tint<color>transparentDroplet base color; the tint prop writes to this variable
--wb-liquid-reveal-wobble<time>6sPeriod of one full bead-shape morph

How it works

  • Refraction: a 0×0 inline SVG inside the container defines feTurbulence (fractalNoise, 2 octaves, fixed seed) feeding feDisplacementMap; the layer holding children applies it with filter: url(#id). The id comes from useId, so multiple instances on a page never collide
  • No per-frame re-renders: displacement and frequency are motion values, and animation frames write straight to SVG attributes without triggering a React render
  • Idle breathing: baseFrequency swings between 10/12 and 16/12 of frequency over 4 seconds; it stops while the component is off-screen to save filter work
  • Three-phase burst: displacement surges to 1.6× restScale within 120ms, then springs to 0 (stiffness 180, damping 18); meanwhile the bead scales to 1.08 and back, flattens out, and throws small droplets at deterministic angles (24–60px, fading over 500ms). Once done, the filter is removed entirely so the content is natively crisp
  • inView fires only on entry: calling reset() while still in view does not burst again immediately; scroll out and back in to replay
  • Safari fallback: Safari's support for SVG displacement filters on HTML elements is unreliable, so when detected it animates a CSS blur(6px) down to 0 with the same timing

Accessibility

  • The filter is purely visual: children stay in the DOM, so text remains selectable, searchable and readable by screen readers
  • With trigger="click" the container is a type="button" button — focusable with Tab, activated with Enter / Space, with a focus-visible ring. Its accessible name is revealLabel before reveal (default 「揭露內容」, "Reveal content") and contentLabel after, falling back to the children text
  • The SVG filter definition, highlight, grain and thrown droplets are all aria-hidden
  • When the user's system has reduce motion turned on, children are shown directly: no filter, no morph, no droplets and no breathing animation