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.
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>"Playground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | — | Content wrapped by the droplet and revealed at the end (text, SVG logos, icon groups) |
revealed | boolean | — | Controlled reveal state; managed internally when omitted |
defaultRevealed | boolean | false | Initial 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 |
restScale | number | 40 | Idle displacement strength (feDisplacementMap scale); higher means stronger distortion |
frequency | number | 0.012 | Base frequency of the refraction texture (feTurbulence baseFrequency); higher means finer ripples |
droplets | number | 8 | Number of small droplets thrown outward on burst (0 for none) |
tint | string | — | Droplet tint (any CSS color); written to --wb-liquid-reveal-tint, transparent when unset |
grain | boolean | false | Overlay fine grain on the surface for a tactile look |
revealLabel | string | "揭露內容" (Reveal content) | Accessible name of the button before reveal when trigger="click" |
contentLabel | string | — | Accessible name after reveal (content description); falls back to the children text when omitted |
ref | React.Ref<LiquidRevealHandle> | — | Exposes reveal() / reset() |
className | string | — | Passed through to the outermost container |
LiquidRevealHandle
| Field | Type | Default | Description |
|---|---|---|---|
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
| Field | Type | Default | Description |
|---|---|---|---|
--wb-liquid-reveal-duration | <time> | 700ms | How long the bead flattens and the tint and highlight fade on reveal |
--wb-liquid-reveal-tint | <color> | transparent | Droplet base color; the tint prop writes to this variable |
--wb-liquid-reveal-wobble | <time> | 6s | Period 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 fromuseId, 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:
baseFrequencyswings between 10/12 and 16/12 offrequencyover 4 seconds; it stops while the component is off-screen to save filter work - Three-phase burst: displacement surges to 1.6×
restScalewithin 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 atype="button"button — focusable with Tab, activated with Enter / Space, with afocus-visiblering. Its accessible name isrevealLabelbefore reveal (default 「揭露內容」, "Reveal content") andcontentLabelafter, 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
Strike Reframe Text
A strike-and-reframe text React component: a sentence is struck through with a hand-drawn line, then a new sentence rises from below to replace it. Chainable for objection-to-answer and old-claim-to-new-claim reframes.
Keyword Converge
A React text animation where a scattered keyword cloud drifts toward the center, merges and resolves into a single concluding sentence — for distilling scattered ideas into one message.