About
A photo that condenses out of scattered points and settles into a dot portrait. Columns, dot size, round or square cells, and a replay key.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Particle Image to your project and fits it to your theme.
Code
How Particle Image is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
import { ParticleImage } from "@/components/ui/particle-image"
const reducedMotion = () =>
typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches
/** Calls `callback` every `ms`, for demos that replay on their own. Reduced motion never does. */
function useEvery(callback: () => void, ms: number) {
const latest = React.useRef(callback)
React.useEffect(() => {
latest.current = callback
})
React.useEffect(() => {
if (reducedMotion()) return
const id = window.setInterval(() => latest.current(), ms)
return () => window.clearInterval(id)
}, [ms])
}
const FIT_CSS = "html,body{overflow:hidden}[data-fit-small] [data-fit-hide]{display:none!important}"
/**
* 21st shows a demo in a small frame: about 674 by 421 on a component page, 358 by 224 on a
* phone and 245 by 139 on a profile card. A demo taller than its frame gets scrollbars, which
* render white on dark pages. FitFrame fills the frame exactly instead.
*
* - "scale": the content shrinks until it fits, centered. For visuals such as orbs.
* - "crop": the content stays at full size from the top and fades out at the bottom. For text
* that streams in, where a changing scale would make the page breathe.
*
* Anything marked `data-fit-hide` (state buttons, the chatcn.dev line) drops out in frames
* shorter than `hideBelow`, so a profile card shows the visual alone.
*/
function FitFrame({
children,
mode = "scale",
hideBelow = 360,
}: {
children: React.ReactNode
mode?: "scale" | "crop"
hideBelow?: number
}) {
const content = React.useRef<HTMLDivElement>(null)
const [cropped, setCropped] = React.useState(false)
React.useLayoutEffect(() => {
const el = content.current
if (!el) return
const measure = () => {
const vw = window.innerWidth
const vh = window.innerHeight
el.toggleAttribute("data-fit-small", vh < hideBelow)
// Layout sizes ignore the transform, so measuring never feeds back.
const w = Math.max(el.offsetWidth, el.scrollWidth)
const h = Math.max(el.offsetHeight, el.scrollHeight)
const scale = mode === "scale" ? Math.min(1, vw / w, vh / h) : 1
el.style.transform = scale < 1 ? `scale(${scale})` : ""
setCropped(mode === "crop" && h > vh + 1)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(el)
window.addEventListener("resize", measure)
return () => {
observer.disconnect()
window.removeEventListener("resize", measure)
}
}, [mode, hideBelow])
const fade = "linear-gradient(to bottom, #000 calc(100% - 40px), transparent)"
return (
<div
className="flex w-full justify-center overflow-hidden"
style={{
height: "100dvh",
alignItems: mode === "crop" ? "flex-start" : "center",
maskImage: cropped ? fade : undefined,
WebkitMaskImage: cropped ? fade : undefined,
}}
>
<style>{FIT_CSS}</style>
<div
ref={content}
className="w-full shrink-0"
style={{ transformOrigin: mode === "crop" ? "top center" : "center" }}
>
{children}
</div>
</div>
)
}
// picsum.photos id 1027, 640 x 800, inline so the 21st preview needs no image host.
const portrait = {
alt: "Portrait of a woman with dark curly hair and red lipstick, looking past the camera in soft light",
width: 640,
height: 800,
src: "data:image/webp;base64,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",
}
type ButtonVariant = "solid" | "outline" | "ghost" | "subtle"
type ButtonSize = "xs" | "sm" | "md" | "lg" | "icon-xs" | "icon-sm" | "icon-md" | "icon-lg"
interface ButtonProps extends React.ComponentProps<"button"> {
variant?: ButtonVariant
size?: ButtonSize
}
const base = cn(
"chatcn-focus inline-flex shrink-0 items-center justify-center gap-1.5 font-medium whitespace-nowrap select-none",
"ease-(--chatcn-ease-standard) transition-[background-color,color,box-shadow,scale] duration-150",
"active:scale-(--chatcn-press-scale) active:duration-100 disabled:pointer-events-none disabled:opacity-50",
"[&_svg]:pointer-events-none [&_svg]:shrink-0",
)
const variants: Record<ButtonVariant, string> = {
solid: "bg-(color:--chatcn-primary) text-(color:--chatcn-primary-fg) hover:bg-(color:--chatcn-primary-hover)",
outline: "bg-(color:--chatcn-card) text-(color:--chatcn-fg) shadow-(--chatcn-shadow-xs) hover:bg-(color:--chatcn-fill-hover)",
ghost: "text-(color:--chatcn-fg-muted) hover:bg-(color:--chatcn-fill-hover) hover:text-(color:--chatcn-fg)",
subtle: "bg-(color:--chatcn-fill) text-(color:--chatcn-fg) hover:bg-(color:--chatcn-fill-hover)",
}
// Heights follow the control scale: 24, 28, 32, 36. Radii stay concentric inside 12 px surfaces.
const sizes: Record<ButtonSize, string> = {
xs: "h-6 rounded-(--chatcn-radius-sm) px-2 text-[0.75rem]/[1rem] [&_svg]:size-3.5",
sm: "h-7 rounded-(--chatcn-radius-sm) px-2.5 text-[0.8125rem]/[1.25rem] [&_svg]:size-3.5",
md: "h-8 rounded-(--chatcn-radius-md) px-3 text-[0.875rem]/[1.25rem] tracking-[-0.003em] [&_svg]:size-4",
lg: "h-9 rounded-(--chatcn-radius-md) px-3.5 text-[0.875rem]/[1.25rem] tracking-[-0.003em] [&_svg]:size-4",
"icon-xs": "size-6 rounded-(--chatcn-radius-sm) active:scale-(--chatcn-press-scale-icon) [&_svg]:size-3.5",
"icon-sm": "size-7 rounded-(--chatcn-radius-sm) active:scale-(--chatcn-press-scale-icon) [&_svg]:size-3.5",
"icon-md": "size-8 rounded-(--chatcn-radius-md) active:scale-(--chatcn-press-scale-icon) [&_svg]:size-4",
"icon-lg": "size-9 rounded-(--chatcn-radius-md) active:scale-(--chatcn-press-scale-icon) [&_svg]:size-4",
}
/** Class names for anything that should look like a button, such as a link. */
function buttonVariants({
variant = "ghost",
size = "md",
}: {
variant?: ButtonVariant
size?: ButtonSize
} = {}) {
return cn(base, variants[variant], sizes[size])
}
function Button({
variant = "ghost",
size = "md",
type = "button",
className,
...props
}: ButtonProps) {
return (
<button
type={type}
data-slot="button"
data-variant={variant}
className={cn(buttonVariants({ variant, size }), className)}
{...props}
/>
)
}
/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
return (
<p data-fit-hide className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
{children}{" "}
<a
href={`https://chatcn.dev/docs/components/${slug}`}
target="_blank"
rel="noopener"
className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
>
chatcn.dev
</a>
</p>
)
}
/** A portrait condensing out of scattered points, again every few seconds. */
export default function ParticleImagePreview() {
const [run, setRun] = React.useState(0)
const replay = React.useCallback(() => setRun((r) => r + 1), [])
useEvery(replay, 5200)
return (
<FitFrame>
<div className="flex w-full flex-col items-center justify-center gap-5 p-6">
<ParticleImage
src={portrait.src}
alt={portrait.alt}
columns={80}
playKey={run}
className="w-full max-w-xs"
/>
<div data-fit-hide>
<Button variant="outline" size="sm" onClick={replay}>
Replay
</Button>
</div>
<MoreOn slug="particle-image">Any image, drawn in points. More on</MoreOn>
</div>
</FitFrame>
)
}







