Sticker cover
A page-transition sheet that rises, swaps the page, pops a sticker and lifts away.
Behaviour
- The sheet rises while the page drifts up 7rem, your change runs, two frames paint, the sticker pops, then the sheet lifts.
- A play() while the sheet is up runs under the same cover (latest wins); one during the lift covers again afterwards. { instant: true } drops it in place for back/forward.
- The sheet is aria-hidden, the page is inert while covered, and focus returns to where it was (or the page box if that element is gone).
- No router dependency and no global attributes; React never re-renders while it plays.
- Reduced motion: a quick crossfade, no drift and no sticker.
Props
| Prop | Type | Description |
|---|---|---|
| ref | Ref<StickerCoverHandle> | ref.current.play(change, { instant }) plays the cover; resolves once it has lifted. |
| sticker | ReactNode | Pops in at the centre once the sheet is up. None: the sheet lifts as soon as the new content has painted. |
| duration | numberdefault 0.5 | Seconds the sheet takes to rise; the lift takes 1.2x. |
| contained | booleandefault false | Cover the nearest relative parent (give it overflow-hidden) instead of the screen. |
| className | stringdefault "bg-foreground" | Classes for the sheet, e.g. its colour (bg-primary). |
| pageClassName | string | Classes for the box around children: the page that drifts up and goes inert. |
Installation
npx shadcn@latest add https://purrfectui.com/r/sticker-cover.jsonHow to use
import { StickerCover, type StickerCoverHandle } from "@/components/ui/sticker-cover";
const cover = useRef<StickerCoverHandle>(null);
<StickerCover ref={cover} sticker={<img src="/sticker.webp" alt="" className="size-40" />}>{page}</StickerCover>
<button onClick={() => cover.current?.play(() => setPage("about"))}>About</button>
// Next App Router (in a client layout): resolve once the new route has committed
const router = useRouter();
const path = usePathname();
const done = useRef<() => void>(undefined);
useEffect(() => done.current?.(), [path]);
const go = (href: string) => href !== path && cover.current?.play(() => new Promise<void>((r) => { done.current = r; router.push(href); }));Source code
Loading…
Credits
Ported from poinu.vercel.app.
License
MIT. Copy it into your project and change it as you like.