Entry points
Two specifiers. revealed is framework-free; revealed/react is the only thing that touches React and ships its own "use client" banner. A UMD/IIFE build is served by unpkg and jsDelivr as the global revealed.
// Core - no React anywhere in this graph.
import {
RevealField,
isSupported,
presets,
DEFAULT_EDGE,
DEFAULT_BRUSH,
DEFAULT_WAVE,
} from "revealed";
import type {
RevealOptions,
ImageSource,
EdgePreset,
EdgeOptions,
BrushOptions,
WaveOptions,
SkeletonOptions,
IdleOptions,
UvRect,
ResolvedBrush,
ResolvedWave,
} from "revealed";
// React.
import { Revealed, useRevealed } from "revealed/react";
import type { RevealedProps, RevealHandle, UseRevealedResult } from "revealed/react";That is the whole surface. Nothing else is exported.
| Export | Type | Default | Description |
|---|---|---|---|
| RevealField | class | - | The vanilla field. See below. |
| isSupported | () => boolean | - | Whether the effect will run here at all. |
| presets | Record<EdgePreset, EdgeOptions> | - | The six edge looks as plain objects, so you can spread one and change a field. Note that it carries only the edge half of a preset: the wave a named preset implies is a brush option, so spreading presets.plain into an edge object gives you its carve, not its lack of momentum. |
| DEFAULT_EDGE | Required<EdgeOptions> | - | Every edge default as a plain object. Spread it to build a UI over the edge fields without hardcoding the numbers in this table. |
| DEFAULT_BRUSH | ResolvedBrush | - | The brush defaults, already resolved: radius, persist, spotlight, trail, healRate, holdTimeout and wave, where healRate is a number (the reciprocal of trail), persist is exactly healRate === 0 and spotlight exactly healRate === Infinity. |
| DEFAULT_WAVE | ResolvedWave | - | The resolved wave on its own - the same object DEFAULT_BRUSH.wave is. Spread it to build a UI over the wave fields without hardcoding the numbers in that table. |
| UvRect | type | - | { x0, y0, x1, y1 } in 0..1 uv of the plate, y down. The shape of measure and idle.region. |
| type-only | … | - | RevealOptions, ImageSource, EdgePreset, EdgeOptions, BrushOptions, WaveOptions, SkeletonOptions and IdleOptions from revealed, plus ResolvedBrush and ResolvedWave, the declared types of the two defaults objects; RevealedProps, RevealHandle and UseRevealedResult from revealed/react. |
RevealOptions
The single options object, accepted by the RevealField constructor, by setOptions as a partial, and spread as props on <Revealed />.
| Option | Type | Default | Description |
|---|---|---|---|
| front | ImageSource | - | Required. The image on top - the one that is eaten away. Also the source of the aspect ratio when aspect is omitted. |
| back | ImageSource | - | Required. The image underneath - the one being revealed. |
| skeleton | ImageSource | SkeletonOptions | - | Line art of the back plate, ghosted onto the front so the visitor knows there is something to uncover. A bare source uses the defaults in SkeletonOptions. |
| aspect | number | from front | Width divided by height of the plates. Read from the front image once it decodes if omitted, and written onto the host as an aspect-ratio - but only if your own CSS has not already given the host one. Pass it explicitly to reserve the box before the image lands and avoid the layout shift. |
| edge | EdgePreset | EdgeOptions | "liquid" | The shape of the cut. A preset name or an object of the same fields; an object is merged over the defaults, not over the last preset. A preset name can also imply a brush.wave - "plain" and "clean" turn the momentum off - which an object never does. |
| brush | BrushOptions | - | Size, lifetime and momentum of the stroke the pointer lays down - including wave, which is where the reveal keeps travelling after the pointer stops. |
| idle | boolean | IdleOptions | true * | The autopilot that paints slow strokes when no pointer is present. false turns it off entirely. * The default flips to false when brush.persist is on - an autopilot that never heals would uncover the whole image by itself. Passing idle yourself always wins. |
| maxDpr | number | 1.5 | Cap on devicePixelRatio: the drawing buffer is the element’s CSS size times min(dpr, maxDpr). Fragment cost scales with the square of it, so 1.5 is the point where a retina plate still looks crisp and a full-width hero still holds 60fps. Raise it to 2 or 3 for a small field, drop it to 1 if a big one stutters. Floored at 0.5. |
| running | boolean | true | False parks the render loop; the canvas keeps its last frame. Visibility and intersection pauses are separate and automatic. |
| progress | number | 0 | A 0..1 floor on the reveal, applied in the display shader rather than the mask - so it never touches what the pointer painted, and the pointer can always run ahead of it. It runs through the same cut a stroke does, which is why a scroll-driven reveal wears the identical wet edge, and it sweeps area roughly linearly rather than wiping top to bottom. With the carve turned off it degrades to a plain top-to-bottom wipe. |
| measure | UvRect | whole image | Rectangle in 0..1 uv that the reveal fraction is measured over - a readout window, never a mask. It changes what onReveal and field.progressreport and nothing at all about what is drawn. Narrow it when the corners of the plate are not part of the story and you want “100% uncovered” to mean the subject, not the margins. |
| onReveal | (fraction: number) => void | - | Called about ten times a second with a smoothed 0..1 number: the fraction of measure uncovered, or the progress floor, whichever is higher. Also called straight after clear() and revealAll(), even if no frame runs in between. It is not a ratchet: it rises while a stroke travels and falls back as the mask heals, which the wave makes obvious. It is monotone only under brush.persist, and under brush.spotlight it reports the footprint of the light - small, nearly constant, and 0 when the pointer leaves - because nothing is accumulated to measure. Latch it yourself if you need a one-way signal. |
| onReady | () => void | - | Called once, after the plates decode and the first frame has painted - or, where the effect is never going to run at all, once the front plate alone has decoded. Either way it means “there is a finished picture on screen now”. |
| onError | (err: Error) => void | - | Called when a degradation is taken and the static front plate is kept instead: no hardware WebGL, a context that could not be created, a shader that would not link, an allocation that failed, or a plate that failed to load. Informational, never fatal. Reduced motion is a preference rather than a failure and does not fire it. |
| pointerTarget | HTMLElement | null | the host | Element the pointer is tracked on, while the coordinates stay relative to the plate. Point it at a larger wrapper when the plate is small and you want a generous hit area, or at a whole section so the reveal starts before the cursor arrives. Point it at the scroll container and a stroke keeps painting past the edges of the image. |
| deferInit | boolean | true | Defers GL bring-up to requestIdleCallback (with a 1.5s timeout, and a 200ms setTimeout where that does not exist) so shader compilation never competes with hydration or first paint. Set it false for a field that is the first thing on the page and should be live the moment it is seen. Read once, at construction. |
React props
<Revealed /> takes everything in RevealOptions plus the five below.
| Prop | Type | Default | Description |
|---|---|---|---|
| …RevealOptions | RevealOptions | - | Every option above is also a prop. front, back and the skeleton source rebuild the field; aspect, edge, brush, idle, maxDpr, running, progress, measure and pointerTarget go through setOptions and land on the next frame; deferInit is read once at construction; the three callbacks are held in a ref, so an inline arrow function is free. |
| className | string | - | Class on the host element the canvas and plates are injected into. |
| style | CSSProperties | - | Inline style on the same host element. |
| fallback | ReactNode | - | Rendered on the server and during the first client render, then dropped the moment the component mounts. It is the server-side placeholder, notthe degradation story: once mounted, the field’s own injected plates are what cover no WebGL, reduced motion and a lost context. Put the front image in here if you want it in the HTML payload. |
| children | ReactNode | - | Rendered over the canvas - captions, buttons, a headline. |
| ref | Ref<RevealHandle> | - | Imperative handle. See RevealHandle. |
ImageSource
Every image slot - front, back, and the skeleton source - takes either a URL or an object with a narrow-viewport swap.
type ImageSource =
| string
| { src: string; small?: string; smallMaxWidth?: number /* default 640 */ };| Field | Type | Default | Description |
|---|---|---|---|
| src | string | - | Required. The URL of the plate. A bare string anywhere an ImageSource is accepted is shorthand for { src }. |
| small | string | - | A narrower plate used at or below smallMaxWidth, for both the texture and the static <picture> behind it. |
| smallMaxWidth | number | 640 | Viewport width in CSS pixels at and below which small wins. The library watches this breakpoint and reloads the texture when it is crossed. |
When small is present the injected plate becomes a real <picture> with a <source media="(max-width: …)">, so the browser picks the file by its own rules and the texture is picked by the same rule. The plate and the texture are never two different images.
EdgeOptions
The shape of the cut between the two plates. Pass an object to edge, or a preset name and then override individual fields by spreading a preset from the presets export.
| Field | Type | Default | Description |
|---|---|---|---|
| scale | number | 7.5 | Frequency of the fbm that carves the boundary. Higher is finer lace. |
| carve | number | 0.38 | How hard the noise chews the boundary, 0..1.5. Zero is a smooth blob. Moved in this release, from 0.62: the wave and the bubbles now supply most of the character, so the old carve double-counted it. edge: "plain" puts it back. |
| detail | number | 0.28 | Amount of the secondary, finer octave. |
| feather | number | 0.012 | Anti-alias width of the cut in field units. 0.012 is a knife edge; raise it for a soft dissolve. Floored at 0.0005, because a zero-width cut has no defined side. |
| threshold | number | 0.18 | Mask value the cut happens at. Lower cuts sooner behind the stroke, so the reveal runs ahead of the brush. Clamped to 0.01..0.99. |
| refraction | number | 0.008 | Rim refraction strength in uv. Zero disables the wet look entirely. Moved in this release, from 0.012: a strong rim fringe fights a faceted, torn boundary. edge: "plain" puts it back. |
| refractionFalloff | number | 55 | How tightly the refraction hugs the rim. Higher is a thinner bead. |
| speed | number | 1 | Drift speed of the noise field. Zero freezes the carve pattern. |
| bubble | number | 0.5 | How hard the banded-noise term punches holes through the reveal, leaving islands of frontstanding inside uncovered ground and detached specks outside it. Zero is the plain carve and nothing else; the useful range is 0..1.2. It is gated on the mask’s own health, so a progress-driven reveal stays clean - the holes only appear where a stroke put them. |
| bubbleScale | number | 2.5 | How many nested level sets one noise hill is wrapped into: higher gives more, smaller holes. Much over 4 it degenerates into visible contour rings. Floored at 1, below which the term is not bands at all but a second carve. |
| facet | number | 0 | Zero reads the mask bilinear and the tear is smooth; 1 reads it on its own grid, giving a low-poly, faceted tear. Fractional values blend the two. This is what shatter uses for its angular edge. |
Edge presets
EdgePreset is the union of these six names. The presets export is the same set as plain objects, so you can build on one:
import { presets } from "revealed";
const sharperInk = { ...presets.ink, feather: 0.006 };| Name | Type | Default | Description |
|---|---|---|---|
| "liquid" | EdgePreset | wave on | Wet, torn, travelling. The reference look - the defaults, unchanged. |
| "plain" | EdgePreset | wave off | Everything the library did before the wave, in one word: the old carve and refraction back, no bubbles, no faceting, and no momentum. carve 0.62 · refraction 0.012 · bubble 0 · facet 0 |
| "dissolve" | EdgePreset | wave on | Fine grain, crumbling edge, the most holes of the six. scale 22 · carve 0.5 · detail 0.4 · feather 0.008 · bubble 0.8 · bubbleScale 4 |
| "ink" | EdgePreset | wave, curling | Slow, heavy, curling blot - the only preset that tunes the wave rather than just switching it, with swirl 0.7 · damping 0.35. scale 4 · carve 0.8 · detail 0.15 · speed 0.4 · refraction 0.02 |
| "shatter" | EdgePreset | wave on | Sharp angular tears, low-poly edge. scale 12 · carve 1.1 · detail 0.05 · feather 0.004 · refraction 0 · bubble 0.3 · facet 1 |
| "clean" | EdgePreset | wave off | No noise, no holes, a plain soft circle. carve 0 · detail 0 · feather 0.02 · refraction 0 · bubble 0 · facet 0 |
BrushOptions
The stroke the pointer lays into the mask.
| Field | Type | Default | Description |
|---|---|---|---|
| radius | number | 0.3 | Radius in aspect-corrected units, where 1 is the image height. |
| trail | number | 2.8 | Seconds a painted stroke takes to fade away completely. The decay is paid out in whole steps, so the window is exact at any refresh rate. Ignored under persist and under spotlight. |
| holdTimeout | number | 130 | Milliseconds of pointer stillness after which painting stops. Ignored under spotlight: with no trail for a held mark to fade into, “where the pointer is” is still a place, so the light goes out when the pointer leaves rather than when it stops. |
| persist | boolean | false | The painted area never fades: once a spot is uncovered, back stays visible there until clear() is called. trail is ignored while this is on, and wave.spread is forced to 0. See the permanent-reveal recipe. |
| spotlight | boolean | false | No trail at all: the reveal is only where the pointer is this frame, and closes immediately behind it - a torch rather than a paint stroke. Exactly healRate: Infinity, the far end of the number persist is the near end of. trail and holdTimeout are ignored, and wave is forced off, because momentum that outlives the frame is a trail. See the torch recipe. |
| healRate | number | from trail | Mask units per second that the reveal heals back - the same dial as trail, from the other end. Omitted it is exactly 1 / trail, so the shipped default is 1 / 2.8; set it directly for a fade slower than trail can express. persist: true is precisely healRate: 0 and spotlight: true is precisely healRate: Infinity. Precedence, coarsest first: persist beats spotlight beats an explicit healRate beats trail. |
| wave | boolean | WaveOptions | true * | Momentum: a stroke throws the reveal forward and it keeps travelling for about a second after the pointer stops. false is exactly { enabled: false }, which is the pre-wave library. * The default flips to false when edge is the "plain" or "clean" preset, and is forced off under spotlight. Fields in WaveOptions. |
WaveOptions
The momentum, passed as brush.wave. The mask’s two spare channels carry a velocity field: a stroke injects into it - forward along the stroke, and rotating in opposite directions on either side of it - and every frame the reveal is dilated along that field while the field itself is transported and damped. No second render target, no extra program, no float textures.
true is the default and false is exactly { enabled: false }. Every field below is a live uniform except resolution, which reallocates the mask.
| Field | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true | Master switch. false - either spelling, the bare wave: false or { enabled: false } - is exactly the pre-wave behaviour. It is derived rather than stored: anything that leaves advect or inject above zero is enabled. |
| advect | number | 2 | How far the reveal is carried along its own velocity each second, in uv at full speed. Zero is no momentum. |
| inject | number | 1 | Velocity a second of full-strength contact puts into the field, along the stroke. This is the term that makes a stroke throw the reveal rather than merely paint it. |
| swirl | number | 0.45 | Zero injects purely along the stroke; 1 purely across it, counter-rotating on either side. The rotational part is where the curls, the vortex-core holes and the detached islands come from. Clamped to 0..1. |
| damping | number | 0.16 | Fraction of the velocity field that survives one second, clamped to 0..0.98 - 1 would be a field that never settles, and the wave has to end. Lower stops sooner. At the default the wave has a life of about 2.5 seconds. |
| spread | number | 0.12 | How fast the reveal creeps into its neighbours, in uv per second. Forced to 0 under persist and clamped under healRate otherwise: it is the only term that adds to the reveal with no stroke behind it, so with nothing healing, a creep of any size would eventually reach every texel on the plate. |
| resolution | number | 512 | Cap on the mask’s long edge while the wave is on - 512 rather than the 1024 it would otherwise use. Lower is more faceted, and cheaper. The only field here that is not a live uniform: changing it reallocates the mask, at exactly the cost of a resize. |
SkeletonOptions
Passing a bare ImageSource to skeleton is shorthand for { src } with everything below at its default.
| Field | Type | Default | Description |
|---|---|---|---|
| src | ImageSource | - | Required. The line-art image. |
| color | string | "#18375d" | Any CSS colour the line art is tinted to. Ignored when source is “image”. |
| opacity | number | 0.18 | Peak opacity of the lines. |
| mode | "draw" | "hold" | "pulse" | "draw" | “draw” sketches a pen front from top to bottom then fades, “hold” is constant, “pulse” breathes. |
| period | number | 5.4 | Seconds for one draw-on, hold and fade cycle. |
| source | "alpha" | "luma" | "image" | "alpha" | How the image becomes coverage: “alpha” uses its alpha channel, “luma” uses 1 minus luminance for black-on-white art, “image” draws it as-is with no tint. |
| reactive | boolean | true | The ghost rides the strokes: coverage climbs to opacity while the pointer or an autopilot stroke is passing, and falls to about a ninth of it on a still, untouched field. false holds it at opacity the whole time - which is what you want if the skeleton looks invisible on a static screenshot. |
IdleOptions
Passing true to idle is shorthand for the defaults; false disables the autopilot.
| Field | Type | Default | Description |
|---|---|---|---|
| enabled | boolean | true * | Autopilot strokes so the effect breathes with no pointer. * false when brush.persist is on - which also means an idle object without enabled inherits that false. Pass idle (bare, i.e. true) or enabled: true to have both. |
| strokes | number | 2 | Concurrent strokes, 1..4. Each one claims a capsule from the frame budget. |
| speed | number | 1 | Stroke travel speed multiplier. |
| region | UvRect | whole image | Rectangle in 0..1 uv the strokes aim through - the one place the autopilot is allowed to work, when you want it hinting at a face rather than wandering the margins. |
| yieldAfter | number | 900 | Milliseconds of pointer inactivity before the autopilot starts spawning again. It stops spawning the instant a pointer arrives, so this is the wait on the way back, not on the way out - and a stroke already in flight is never cut off, because ending a sweep mid-frame is a visible pop. |
RevealField
The whole vanilla API. The constructor injects a canvas and the two static plates into host, giving it position: relative if it is currently static.
const field = new RevealField(host, {
front: "/demo/front.webp",
back: "/demo/back.webp",
});- new RevealField(host: HTMLElement, options: RevealOptions)
- Builds the field. Nothing is drawn until the plates decode; if
deferInitis true, GL bring-up waits for an idle callback. - setOptions(next: Partial<RevealOptions>): void
- Merges new options in, on the same instance and the same context. A change of image source reloads that plate; a change of the skeleton’s
sourceormoderelinks the display shader; a change ofbrush.wave.resolutionreallocates the mask, at exactly the cost of a resize. Every other field is a uniform and applies on the next frame - includingbrush.persist, which is why freezing and thawing a reveal costs nothing. - paint(x: number, y: number, strength?: number): void
- Lays a stroke at a point in 0..1 uv of the plate, exactly as a pointer would.
strengthdefaults to full. - clear(): void
- Wipes the mask back to fully covered - only the front plate is visible. Unconditional: with
brush.persiston nothing heals, so this is the only way back. - revealAll(): void
- Floods the mask so the back plate is fully visible. It then heals like any other stroke, over
brush.trail- so if you want it to stay revealed, pair it withbrush.persist. Underbrush.spotlightit shows everything for exactly one frame before the next one heals it away. - play(): void
- Resumes the render loop. Equivalent to
running: true. - pause(): void
- Parks the render loop and releases the animation frame. The last frame stays on screen.
- readonly progress: number
- The last measured 0..1 fraction - the same number handed to
onReveal. - readonly supported: boolean
- Whether this instance actually brought up hardware WebGL. False means the static plates are what the visitor is looking at.
- destroy(): void
- Frees textures, framebuffers, programs and buffers, disconnects every observer and listener, and cancels the animation frame. Call it on unmount.
RevealHandle
What a ref on <Revealed /> resolves to: the imperative subset of the field, plus the instance itself.
- paint(x, y, strength?) · clear() · revealAll() · play() · pause()
- Forwarded straight to the underlying RevealField. Safe to call before the field exists - the calls are simply dropped.
- field: RevealField | null
- The instance, once it has been constructed - which is null on the server and on the first client render, and non-null from mount onwards. It stays non-null when WebGL is refused: the field still exists, still owns the static plates, and simply reports
supported: false.
useRevealed
The same machinery for people who want to own the DOM. Pass a ref to your own host element and the options; you get the field and its live status back.
"use client";
import { useRef } from "react";
import { useRevealed } from "revealed/react";
export function CustomHost() {
const host = useRef<HTMLDivElement>(null);
const { field, supported, ready, progress } = useRevealed(host, {
front: "/demo/front.webp",
back: "/demo/back.webp",
});
return (
<div ref={host} className="my-own-stage" data-ready={ready}>
{!supported && <img src="/demo/front.webp" alt="" />}
<span>{Math.round(progress * 100)}%</span>
<button type="button" onClick={() => field?.clear()}>Reset</button>
</div>
);
}| Key | Type | Default | Description |
|---|---|---|---|
| field | RevealField | null | - | The instance, or null before it exists. |
| supported | boolean | - | Whether hardware WebGL came up for this host. |
| ready | boolean | - | True once the plates have decoded and the first frame painted. |
| progress | number | - | The live 0..1 revealed fraction, re-rendered about ten times a second. |
isSupported
Returns whether this browser will actually run the effect. It is false in exactly three cases: on the server, without a WebGL context backed by real hardware, and under prefers-reduced-motion: reduce. In every one of them a field still renders its static front plate.
import { isSupported } from "revealed";
isSupported(); // false on the server, on software GL, and under reduced motionThe hardware verdict is asked once per page and cached - the throwaway context is created and released on the first call. The reduced-motion half is re-read every time, so a visitor who changes the preference mid-session gets a truthful answer without a reload.