Ticket Stub

A card torn along a perforation, with a notch bitten out of the paper at each end of it.

Loading preview

Installation

npx shadcn@latest add @loomui/ticket-stub

Usage

import { TicketStub } from "@/components/ui/ticket-stub"
<TicketStub stub={<Seat number="14A" />}>
  <BoardingPass from="LIS" to="KIX" />
</TicketStub>

children is the ticket and stub is the part that tears off. Both are your own markup. The component supplies the paper, not the layout.

The notches

Two radial gradients, each transparent inside its radius, used as a mask. The masks are composited with intersect rather than stacked, because layered masks add up. Without it only the last one declared takes a bite, and the ticket comes out notched at one end.

<TicketStub split={70} notch={12} />

split is where the tear falls as a percentage of the long side, and notch is the radius of the bite in pixels. The perforation is inset by that same radius at both ends, so the dashes start where the paper does rather than running out into the hole.

The edge

Everything the edge is made of sits on a wrapper. A box-shadow or a border on the masked element is cut away along with the paper, which leaves the notches unoutlined and the ticket flat.

The outline is four drop shadows offset a pixel in each direction, in --border. A filter traces the element's alpha, so the line follows the curve of every bite rather than the rectangle the paper started as. It is also the only edge that survives dark mode, where a card the same value as the page has nothing but its outline to separate the two.

Orientation

<TicketStub orientation="vertical" stub={...}>...</TicketStub>

horizontal is a wide ticket with the stub on the right and the tear running top to bottom. vertical is a tall one with the stub below and the tear running across. The notches and the perforation both follow.

lift={false} drops the hover. It is a small rise and a fraction of a degree of rotation. Enough to read as paper, not enough to read as a card game.

Props

PropTypeDefaultDescription
stubReactNodenoneThe torn-off part, after the perforation.
splitnumber68Where the tear falls, as a percentage.
notchnumber12Radius of the notches, in pixels.
orientation"horizontal" | "vertical""horizontal"Stub to the right, or below.
liftbooleantrueLift the ticket off the page on hover.
classNamestringnoneMerged onto the wrapper. Size goes here.

Any other <div> prop is forwarded.

Accessibility

The perforation is aria-hidden, and the mask only removes paper, never content. It does remove paper though, so keep text clear of the notches or it gets clipped too.