Ticket stub
FreeA ticket with a perforated stub. Checking in tears the stub off along the perforation, so used is something you can see on the ticket and not only read. The tear is the feedback; the plain text line on the ticket is the record.
pnpm dlx shadcn@latest add @caliper/ticket-stubPress Check in, or pull the stub down.
When to use #
Use it for
- Check-in, redeem and validate flows that cannot be repeated by accident.
- Showing that a booking, coupon or pass has been used.
Skip it when
- Reversible toggles; use a switch.
- Anything that must stay readable for years; the ticket is a status, not an archive.
Decisions #
Why it is built this way, so you know what to keep when you change your copy.
Behaviour
- Nothing depends on dragging
- Check in is a button with a live status. The stub can also be pulled down with a mouse or finger, past 60px it tears.
- Undo takes focus
- After the tear the Check in button is replaced by Undo, and focus lands on it so the next key press is the way back.
Styling
- The tear is the only animation
- Under reduced motion the stub is gone at once and the ticket keeps its text line.
Installation #
pnpm dlx shadcn@latest add @caliper/ticket-stub
Adds its 7 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.
Usage #
This is the code behind the preview above.
import { useState } from "react" import { TicketStub } from "@/components/ui/ticket-stub" export function TicketStubDemo() { const [used, setUsed] = useState(false); return ( <div className="w-full max-w-md"> <TicketStub title="Halden Rail · 18:42 to Porto" meta={["Gate B", "Seat 14C"]} code="14C" used={used} onUse={() => setUsed(true)} onUndo={() => setUsed(false)} usedNote={<><b>Checked in 14:02</b> · stub kept by the gate</>} > Valid for entry · 1 passenger </TicketStub> </div> ); }
API reference #
TicketStub
component
Accessibility #
It exposes group semantics, with aria-label, aria-hidden and aria-live kept in step with state. Changes that matter are announced through a live region.
No custom key handling; Tab and the native activation keys do the work.
Motion #
It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.