71 / Scenes
Burst
A short burst of pieces for the moment something finally completes. It draws nothing until it is fired and stops as soon as the last piece falls out of the box.
One step left on this invoice.
"use client"
import * as React from "react"
import { Burst, type BurstHandle } from "mischief-ui/burst"
export function BurstDemo() {
const burst = React.useRef<BurstHandle>(null)
const [done, setDone] = React.useState(false)
return (
<div className="border-border bg-card relative isolate grid w-full max-w-md place-items-center overflow-hidden rounded-[var(--radius)] border px-6 py-14">
<Burst ref={burst} announce="Invoice paid" />
<p className="text-muted-foreground mb-4 text-sm">
{done ? "Invoice paid." : "One step left on this invoice."}
</p>
<button
type="button"
className="bg-primary text-primary-foreground focus-visible:ring-ring inline-flex min-h-11 items-center rounded-full px-5 text-sm font-semibold focus-visible:ring-2 focus-visible:outline-none"
onClick={() => {
setDone(true)
burst.current?.fire()
}}
>
Mark as paid
</button>
</div>
)
}Installation
Copy the source into your project, or keep it behind a package.
npx shadcn@latest add Tinkerers-Labs/mischief-ui/burstimport { Burst } from "mischief-ui/burst"Or paste it in yourself. The source imports the shared cn helper from @/lib/utils, so point that at your own copy.
"use client" import * as React from "react"import { RenderSurface, useThemeColors, type SurfaceColor,} from "@/registry/default/render-surface/render-surface"import { cn } from "@/lib/utils" export type BurstOptions = { /** Where the burst starts, in pixels from the top left of the box. */ x?: number y?: numberUsage
export function Invoice() {
const burst = React.useRef(null)
return (
<div className="relative isolate overflow-hidden">
<Burst ref={burst} announce="Invoice paid" />
<button onClick={() => burst.current?.fire()}>Mark as paid</button>
</div>
)
}It costs nothing while it waits
Most celebration components animate a canvas continuously and simply draw nothing most of the time. This one keeps its loop paused until fire is called and pauses it again on the first frame where no piece is left alive, so a page holding one of these is running no animation at all until the moment it matters.
What happens under reduced motion
fire draws nothing. A burst is pure movement, and a still frame of one is a pile of rectangles that means nothing to anybody.
The announcement still happens. That is the point of the announce prop: the information a burst carries -- this worked -- reaches a reader who is not going to see it, whether they turned motion off or are using a screen reader.
Where to put it
It covers its nearest positioned ancestor and ignores the pointer, so it belongs inside the region you want the pieces to fall through rather than at the root of the page. Give that ancestor a positioning context, and overflow hidden if you would rather the pieces did not spill.
<Burst ref={burst} announce="Plan upgraded" count={90} velocity={520} />API
colorsstring[]Theme custom properties or CSS colours. Defaults to ["--primary", "--foreground"].countnumberPieces per burst. Defaults to 60.velocitynumberPixels per second the pieces leave at. Defaults to 420.gravitynumberPixels per second squared. Defaults to 900.announcestringAnnounced once when a burst is fired. Leave it out when the burst is decorating something already announced elsewhere.BurstHandle
What the ref gives you.
fire(options?: BurstOptions) => voidStarts a burst. Without options it comes from the centre of the box.BurstOptions
xnumberPixels from the left of the box.ynumberPixels from the top of the box.countnumberOverrides the piece count for this burst.Accessibility
The canvas is decoration and is hidden. The announce prop puts the meaning of the burst into a polite live region, which is what a reader using a screen reader or a reader with motion turned off actually receives. Nothing about the burst is required to operate anything.