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.
@keyframeui
registry/demos/paint-reveal-work-demo.tsx
import { PaintReveal } from "@/components/ui/paint-reveal";
const projects = [
{ client: "Nordlys", kind: "Holiday homes", year: 2026, photo: "1600607687939-ce8a6c25118c" },
{ client: "Atelier Sund", kind: "Brand identity", year: 2025, photo: "1515886657613-9f3515b0c78f" },
{ client: "Kvarter", kind: "Office fit-out", year: 2025, photo: "1524758631624-e2822e304c36" },
{ client: "Fjell", kind: "Campaign", year: 2024, photo: "1506905925346-21bda4d32df4" },
];
const photo = (id: string, grade = "") =>
`https://images.unsplash.com/photo-${id}?auto=format&fit=crop&w=900&h=675&q=80${grade}`;
export function PaintRevealWorkDemo() {
return (
<section className="w-full max-w-4xl">
<div className="mb-6 flex items-end justify-between gap-6">
<h2 className="text-2xl font-semibold tracking-tight text-zinc-900 sm:text-3xl dark:text-zinc-50">
Selected work
</h2>
<a href="#" className="text-sm text-zinc-500 transition-colors hover:text-zinc-900 dark:hover:text-zinc-100">
All projects →
</a>
</div>
<ul className="grid gap-x-5 gap-y-8 sm:grid-cols-2">
{projects.map((project) => (
<li key={project.client}>
<a href="#" className="group block">
<PaintReveal
src={photo(project.photo, "&sat=-100&con=6")}
alt={`${project.client}: ${project.kind}`}
revealSrc={photo(project.photo)}
brushSize={130}
softness={0.6}
fadeMs={700}
autoPlay={false}
className="aspect-[4/3] w-full rounded-lg"
/>
<div className="mt-3 flex items-baseline justify-between gap-4 text-sm">
<span className="font-medium text-zinc-900 dark:text-zinc-100">{project.client}</span>
<span className="text-zinc-500 tabular-nums">
{project.kind} · {project.year}
</span>
</div>
</a>
</li>
))}
</ul>
</section>
);
}
Live preview, examples and props: https://keyframeui.com/components/paint-reveal#paint-reveal-work-demo




















