Paint Reveal

Two pictures in one frame. The pointer paints the second one in with a soft brush, and the stroke closes back behind it.

  • Image
  • Hover
  • Canvas

Uses clsx, tailwind-merge

Inspired by Lando Norris

Open in ChatGPTOpen in Claude
Clouds of ink drifting through water, in black and whiteThe same ink clouds in red, orange and electric blue
Tinta

Brand & motion studio · Lisbon

Color that stays with you.

Identities, campaigns and the motion between them, for brands that want to be remembered.

Installation

Add the component on its own with the CLI, or copy the file in by hand.

The CLI writes components/ui/paint-reveal.tsx and installs clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.

npx shadcn@latest add @keyframeui/paint-reveal

Examples

Finished sections built with Paint Reveal. Each one installs whole, with the component included.

Photo Restore

An AI photo-restoration tool. With `fadeMs={Infinity}` the paint stays, so you restore the print stroke by stroke. "Restored" floods the rest in from the switch, and "Faded" floods it back and starts over.

A faded, blurry print of a mountain lake with a rowing boatThe same lake restored: turquoise water, sharp peaks, a clear sky
Lago di Braies, 1974Paint over the print to restore it

About the Founder

A studio's about section. With `autoPlay`, the brush drifts over the portrait by itself until the pointer takes over, so the color keeps moving through it while you read.

Portrait of Noor Haddad

About the founder

“Every brand guide I was handed was a PDF nobody opened. So we build the system into the tools people already use.”

Noor Haddad, founder of Mera Studio

Twelve years in brand teams at Spotify and Monzo, then a studio of six in Amsterdam. Mera designs identities that ship as code, type and templates on day one.

Work Grid

A portfolio grid in grayscale, with a smaller brush on each card. Only the card under the pointer draws anything, and `autoPlay={false}` keeps the grid still on phones.

Props

srcRequired

Typestring

The picture shown at rest.

altRequired

Typestring

Alt text for src.

revealSrcRequired

Typestring

The picture the brush paints in. Give it the same size and framing as src so the two line up. Any URL works: it's drawn to a canvas but never read back, so it needs no CORS headers.

revealAlt

TypestringDefault""

Alt text for revealSrc. Leave it empty when it only restyles src.

brushSize

TypenumberDefault180

Diameter of the brush, in px. A quick stroke paints up to a fifth wider.

softness

TypenumberDefault0.5

Edge of the brush, from 0 (crisp) to 1 (very soft). Strokes are drawn as one shape, so overlaps never build up into darker spots.

fadeMs

TypenumberDefault600

How long a stroke takes to close back, in ms. It holds its width at first, then thins toward the tail. 0 leaves no trail, just a spotlight under the pointer; Infinity keeps the paint until the next flip.

autoPlay

Typeboolean | "touch"Default"touch"

Let the brush drift over the frame by itself while it's in view. "touch" does it only on screens without hover, true also on desktop while the pointer is elsewhere, false never. A finger drag or the pointer takes over.

revealed

Typeboolean

Floods revealSrc over the whole frame, from the brush, the toggle or the center. After that the brush paints src back. Leave unset to let the toggle manage it.

defaultRevealed

TypebooleanDefaultfalse

Whether revealSrc starts out covering the frame.

onRevealedChange

Type(revealed: boolean) => void

Called when the toggle flips the pictures.

toggle

TypebooleanDefaultfalse

Show a two-way switch in the bottom-right corner. It's always shown with reduced motion, where the brush is off and the switch crossfades.

labels

Type[string, string]Default["Before", "After"]

The switch's labels: src first, then revealSrc.

toggleClassName

Typestring

Classes for the switch, e.g. top-3 bottom-auto to move it to the top-right corner.

objectPosition

TypestringDefault"50% 50%"

Where both pictures are anchored when they're cropped, like CSS object-position. Takes keywords and percentages.

children

TypeReact.ReactNode

Content laid over the pictures, like a headline. It doesn't block the brush, and in-flow content can set the frame's height.

className

Typestring

Classes for the frame. Give it a size here (aspect-[3/2], h-[80svh]) unless its children set one. Touch screens can still scroll the page through it vertically; add touch-none to paint in every direction instead.