Design Lounge
Install

Nº 192 of 520

Cinema ticket with tear-off stub

A 1930s picture-house ticket in cream and oxblood. Pull the perforated stub away and it tears along a ragged edge, keeps itself, and stamps the ticket Admitted.

Preview

Open full size
1280 × 800 · 11.3 KB · 182 lines

Use it

Free to use · credit appreciated

1 Once, in your project

npx skills add SusanAcharya/Design-Lounge

2 Then ask

Using the Design Lounge skill, build "Cinema ticket with tear-off stub" (piece card-cinema-ticket) in my stack. Match its brief; restyle it onto my product's theme.

No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.

Brief

210 lines

Build brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.

What it is

A single cinema ticket for a fictional art deco picture house, the Marlowe Picture House, est. 1931. It lies on an oxblood velvet stage under a warm spotlight. The left part is the ticket: house name, a poster panel with a gold sunburst and the film title “The Lantern at Low Tide” in Limelight, and a row of facts (date, doors, screen, fare). The right part is a 180px stub with “Admit one”, row F, seat 14 and serial Nº 031742, joined to the ticket by a dotted perforation with two semicircle notches. The user grabs the stub and pulls it away. It hinges open from the top, and once pulled far enough it tears: both edges go ragged, the stub drops a little to the side and stays as a keepsake, and a red double-ruled “Admitted” stamp lands on the ticket. The detail worth copying is that the tear happens on release, not on threshold, so a hesitant pull snaps back.

Reference behaviour

  1. First frame: the ticket is whole and centred, 700 × 340 at scale 1.22 on a 1280 frame (854 × 415 on screen). Above it, the eyebrow “Tonight in the Gilt Room”. Below, the hint “Pull the stub away to tear it · → nudges · Enter tears”.
  2. Pointer down on the stub captures the pointer. Moving right or down opens the stub. Progress p = (max(0, dx) + max(0, dy) × 0.5) / 150, clamped 0–1, with dx/dy divided by the current scale.
  3. While dragging, the stub’s transform is translateX(p × 6px) rotate(p × 12deg) with transform-origin: 0 100% (bottom of the perforation). The gap opens at the top first, as paper does. No transition while dragging.
  4. Release with p < 1: the stub springs back to 0 over 420ms on expo out.
  5. Release with p ≥ 1: tear. Both the ticket’s right edge and the stub’s left edge get a ragged clip-path polygon (a point every 10px, inset 3–10px from the line, seeded so it is the same every time). The stub moves to translate(48px, 34px) rotate(7deg) over 640ms expo out and is disabled.
  6. On tear, the “Admitted” stamp fades from 0 to 0.85 opacity and settles from rotate(-11deg) scale(1.6) to scale(1) over 320ms expo out, 260ms after the tear.
  7. On tear, the hint changes to “Stub kept. Enjoy the picture.” and a “Reprint ticket” button appears and takes focus.
  8. Reprint: clip-paths clear, stamp fades out, stub returns to the perforation, the stub is enabled and focused again.
  9. Keyboard on the stub: ArrowRight or ArrowDown adds 0.25 to p (four presses tear). ArrowLeft, ArrowUp or Escape resets to 0. Enter or Space tears at once.
  10. Reduced motion: no transitions. Drag still previews the hinge (it follows the finger), tear and reprint jump to their end states.

Structure

1280 × 800, body grid centred, gap 28px
  eyebrow 13px, tracked .32em, gold
  .fit  width min(854px, 100%), height = 340 × k
  ┌────────────────────────────── 520 ──────────────────────────╮╭── 180 ──────┐
  │ MARLOWE PICTURE HOUSE                            EST. 1931   ○  RETAIN THIS │
  │ ─────────────────────────────────────────────────────────── │  ADMIT ONE  │
  │ ╔═══════════════════ poster (double rule) ═════════════════╗ :             │
  │ ║      A picture in eleven reels   (sunburst behind)        ║ :  ROW  SEAT  │
  │ ║          THE LANTERN / AT / LOW TIDE  42px                ║ :  F    14    │
  │ ║          VERA HALLORAN · TEODOR WYNN                      ║ :             │
  │ ╚═══════════════════════════════════════════════════════════╝ :  Nº 031742  │
  │ DATE          │ DOORS    │ SCREEN │ FARE                      :  Screen Two  │
  │ Sat 17 Oct    │ 7:40 pm  │ Two    │ £9.50 · Stalls           ○              │
  └──────────────────────────────────────────────────────────────╯╰─────────────┘
  caption row (hint + Reprint), min-height 44px
  • .fit is a sized box. .ticket inside is position: absolute, 700 × 340, transform: scale(k) with transform-origin: 0 0. k = min(1.22 on wide screens, fit width / 700). Absolute positioning keeps the unscaled 700px out of layout so nothing overflows at 375px.
  • .main is an article labelled “Cinema ticket, The Lantern at Low Tide”. Inside: .house row, .poster with an h1, and a dl.facts of four pairs.
  • .stub is a button (it is the control), absolutely placed at left 520px. It holds “Admit one”, a dl for row and seat, and the serial.
  • .stub-ring is a sibling span that draws the focus ring, because the stub’s own outline is clipped by its mask.
  • A visually hidden p[aria-live=polite] announces progress and the tear.

Tokens

:root {
  --stage: #3b1418;        /* velvet, top */
  --stage-2: #24090c;      /* velvet, bottom */
  --paper: #f2e4c4;        /* ticket stock */
  --paper-2: #e8d5ab;
  --ink: #2b1b14;          /* black print */
  --ink-2: #5a4334;        /* labels */
  --print: #9c2a22;        /* second print colour: title, stamp, serial */
  --gold: #b8862f;         /* sunburst, focus, button rule */
  --chalk: #f6ead0;        /* text on the stage */
  --display: "Limelight", Georgia, serif;
  --text: "Spectral", Georgia, serif;
  --notch: 14px;           /* notch radius */
  --perf-dot: 2.6px;       /* perforation hole radius, 13px pitch */
  --expo: cubic-bezier(0.16, 1, 0.3, 1);
  --std: cubic-bezier(0.2, 0.7, 0.2, 1);
  --t-spring: 420ms;
  --t-tear: 640ms;
  --t-stamp: 320ms;
}

Stage background: a radial warm spot rgba(255,196,120,.16) at 50% 42%, a vertical curtain stripe (repeating-linear-gradient(90deg, …) with a 92px period), over a --stage → --stage-2 gradient.

Typography

RoleFamilySizeWeightTrackingCase / colour
EyebrowSpectral13px400.32emupper, #d9b97a
House rowSpectral11px400.28emupper, --print
TaglineSpectral italic13px4000--ink-2
Film titleLimelight42px / 0.95400.02emupper, --print
Title “at”Limelight20px400.3emupper, --ink
CastSpectral11px400.24emupper, --ink-2
Fact labelSpectral10px400.24emupper, --ink-2
Fact valueSpectral19px / 1.26000lining numerals
Admit oneLimelight26px400.06emupper, --print
Row / seatLimelight40px4000--ink
SerialSpectral15px600.14em--print, tabular
StampLimelight24px400.12emupper, --print, multiply

Limelight is the period poster face. Use it only for the title, “Admit one”, the seat and the stamp. Everything else is Spectral.

Motion

ThingTriggerPropertyFrom → toDuration / easing
Stub hingepointer move / arrowstransformrotate(0) → translateX(6px) rotate(12deg)follows pointer, no transition
Stub snap backrelease with p < 1transformcurrent → none420ms expo out
Stub keepteartransformnone → translate(48px,34px) rotate(7deg)640ms expo out
Ragged edgestearclip-pathnone → polygoninstant
Stamptearopacity, transform0, scale 1.6 → .85, scale 1160ms / 320ms expo out, 260ms delay
Reprintclickall of the aboveend → startsame durations

Reduced motion: all transitions removed. The hinge still follows the pointer because it is direct manipulation, not animation.

States

  • Stub resting: cursor: grab. Dragging: cursor: grabbing, transitions off (.dragging).
  • Stub focus-visible: a 2px --gold ring 6px outside the stub (.stub-ring), because the mask clips a normal outline.
  • Partially pulled: hinge angle shows progress; live region says “Tear 50 percent”.
  • Torn: .is-torn on the ticket. Stub disabled, aria-label “Stub torn and kept, row F seat 14”, cursor default. Stamp visible. Reprint button visible.
  • Reprint hover: background rgba(184,134,47,.18).
  • There is no error state. A tear is final until Reprint.

Accessibility

  • The stub is a real button with aria-label="Ticket stub, row F seat 14. Drag or press Enter to tear" and aria-describedby pointing at the hint.
  • Keys: ArrowRight / ArrowDown nudge 25%, ArrowLeft / ArrowUp / Escape reset, Enter / Space tear. Arrows call preventDefault so the page does not scroll.
  • Live region (polite): “Tear 25 percent”, “Ticket torn. Admitted.”, “Fresh ticket printed.”
  • Focus moves to Reprint after a tear and back to the stub after reprint, so keyboard users never land on a disabled control.
  • Contrast: --ink on --paper is 13:1. --print on --paper is 6.4:1. --ink-2 labels are 7:1. The caption #e2cfa8 on --stage is 9:1.
  • Reprint button is 40px tall. The stub is 180 × 340 at scale 1.

Responsive rules

  • ≥1280: scale 1.22.
  • 1024: the 854px cap still fits; scale 1.22.
  • 768: scale = (768 − 32) / 700 ≈ 1.05, capped at 1 below 1000px wide.
  • <640: scale = width / 700 (0.49 at 375). Still legible because the ticket is one object. Do not restack the stub under the ticket: the perforation is vertical by design.
  • Body is overflow: hidden. The kept stub may poke past the edge at 375. That is fine; it never causes horizontal scroll because the ticket is absolutely positioned.

Acceptance checklist

Always

  • One ticket, one stub, joined by a vertical perforation with two 14px semicircle notches cut through the paper (mask, not painted circles).
  • Drag hinges the stub around its bottom-left corner. Release below threshold snaps back.
  • Tear happens only on release at full progress, or on Enter / Space, or after four arrow presses.
  • After a tear both edges are ragged and the stub is kept beside the ticket, not removed.
  • A stamp confirms admission. A Reprint control restores the start state.
  • Stub is a focusable button with a visible ring. Focus moves sensibly after tear and reprint.
  • Live region announces progress and tear.
  • No horizontal scroll at 375px.
  • Reduced motion removes transitions but keeps the drag preview.

This demo

  • House “Marlowe Picture House · Est. 1931”, film “The Lantern at Low Tide”, cast “Vera Halloran · Teodor Wynn”.
  • Facts: Sat 17 Oct · 7:40 pm · Two · £9.50 · Stalls. Stub: Row F, Seat 14, Nº 031742.
  • Ticket is 700 × 340, main 520px, stub 180px, scale 1.22 at 1280.
  • Stamp reads “Admitted / Usher 07 · 7:32 pm” in #9c2a22, rotated −11°.
  • Progress formula uses 150px and halves the vertical component.

Implementation notes

Notches are cut with two half-height masks per part. The ticket gets circles on its right corners, the stub on its left:

.main {
  mask:
    radial-gradient(circle at 100% 0,    #0000 14px, #000 14.5px) top    / 100% 51% no-repeat,
    radial-gradient(circle at 100% 100%, #0000 14px, #000 14.5px) bottom / 100% 51% no-repeat;
}
.stub { /* same with 0 0 and 0 100% */ transform-origin: 0 100%; touch-action: none; }
.ticket { filter: drop-shadow(0 18px 24px rgba(0,0,0,.45)); } /* shadow follows the mask */

Use drop-shadow on the parent, not box-shadow on the parts, or the shadow ignores the notches and the ragged edge.

The tear polygon is seeded so it is stable:

function jag(side) {
  const pts = []; let s = 7;
  for (let y = 0; y <= 340; y += 10) { s = (s * 9301 + 49297) % 233280; pts.push(3 + (s / 233280) * 7, y); }
  return side === 'main'
    ? 'polygon(0 0,' + pts.reduce((a, v, i) => i % 2 ? a : a + `${520 - pts[i]}px ${pts[i + 1]}px,`, '') + '0 340px)'
    : 'polygon(' + pts.reduce((a, v, i) => i % 2 ? a : a + `${pts[i] - 3}px ${pts[i + 1]}px,`, '') + '180px 340px,180px 0)';
}

Both parts use the same seed, so the edges are complementary.

Drag maths must undo the scale, or the stub tears twice as fast on a small screen:

stub.addEventListener('pointerdown', e => { start = { x: e.clientX, y: e.clientY }; stub.setPointerCapture(e.pointerId); });
stub.addEventListener('pointermove', e => {
  if (!start) return;
  const dx = (e.clientX - start.x) / k, dy = (e.clientY - start.y) / k;
  show((Math.max(0, dx) + Math.max(0, dy) * 0.5) / 150);
});
stub.addEventListener('click', e => { if (e.detail === 0) tear(); }); // keyboard Enter / Space only

Common mistakes:

  • Tearing as soon as progress hits 1 mid-drag. The user loses the chance to change their mind.
  • Removing the stub from the DOM after the tear. Keep it; it is the souvenir.
  • Painting the notches as stage-coloured circles. The stage is a gradient, so they show.
  • Setting the poster title in a generic display sans. The piece is the period face.
  • Letting the inline transform from the drag override the torn-state class. Clear it before adding .is-torn.

Details

Palette
Type
Limelight · Spectral
Motion
Subtle motion
Build
One session
Designed bySusan AcharyaKathmandu

Studied

Bermawy

Let one toy be the real control. Keep the rest plain and on paper.

All sources

On the shelves