Navigation

v0.0.4
Materials

Ticket

A sheet with a tear line: a pass, a coupon, a receipt and its stub.

On this page

A ticket is a sheet with a tear line: a boarding pass, an event ticket, a voucher, a receipt and its stub. Two raised faces meet on a perforation, the body and the stub that would be torn off, and a half-round notch is punched into each edge where they meet. It is a material, like a card: what stands on it is yours.

Ticket

<article class="ticket w-full max-w(300)">  <div class="ticket-body">    <p class="eyebrow">Boarding pass</p>    <div class="flex items-end justify-between mt(8)">      <div>        <p class="heading heading-lg">TPE</p>        <p class="m(0) fg-text-muted text-sm">Taipei</p>      </div>      <svg class="icon mb(22)" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">        <path d="M4 12h16m0 0-5-5m5 5-5 5" />      </svg>      <div class="text-right">        <p class="heading heading-lg">HND</p>        <p class="m(0) fg-text-muted text-sm">Tokyo</p>      </div>    </div>    <dl class="data-list mt(16)">      <div class="data-list-item">        <dt class="data-list-term">Date</dt>        <dd class="data-list-value">12 November</dd>      </div>      <div class="data-list-item">        <dt class="data-list-term">Boarding</dt>        <dd class="data-list-value">09:10 · Gate B7</dd>      </div>    </dl>  </div>  <div class="ticket-stub flex items-center justify-between">    <div>      <p class="m(0) fg-text-muted text-xs">Seat</p>      <p class="m(0) fg-text-strong font-medium">14A</p>    </div> <code class="code">MK8Q2X</code>  </div></article>

#The tear line

The body and the stub are two sheets, each with a corner of its own. Where the browser has corner-shape, the two corners of each face that lie on the tear line are scooped: a quarter circle cut into the sheet instead of rounded off it. Two scoops side by side make a half-round notch, and the contour and the shadow follow the notch, because they follow the corner. No mask is involved, so nothing is cut that should not be: a focus ring inside a ticket is whole.

Each face is cut straight along the tear line, so neither draws a contour across it. The only line there is the perforation, a row of 4px dashes that runs between the notches.

Without corner-shape: scoop the two faces meet square, and the perforation alone parts them.

#A stub at the end

ticket-row sets the stub at the end of the body instead of under it, the way a coupon is printed. The stub is as wide as what it holds, and the body takes the rest.

Row

<article class="ticket ticket-row w-full max-w(420)">  <div class="ticket-body">    <p class="heading">20% off your first order</p>    <p class="m(0) fg-text-muted text-sm mt(4)">Valid until 30 November on orders over $40.</p>  </div>  <div class="ticket-stub flex items-center">    <button class="btn btn-sm btn-primary" type="button">Redeem</button>  </div></article>

#Geometry

A ticket has the corner of a surface, 8px notches and 20px of padding in each face. --ticket-notch sets the radius of the notch and --ticket-padding the padding; a receipt that is mostly figures may want less. The sheet publishes --surface-color, so a badge's ring or a stack of avatars on it is cut in the right color.

#Ticket or card

A ticket says "this is yours to hand over": something issued to one person and checked, redeemed or torn. Anything else on a raised sheet is a card. Do not use the notches as decoration for ordinary content: the shape means something.

#Accessibility

  • The shape is decoration. Use the element that fits the content: an <article> for a pass or a voucher, with a heading inside it.
  • A code a person reads aloud or types belongs in code, which sets it in the mono face. Offer a way to copy it.
  • In forced colors each face takes a system outline and the perforation is a solid line.

#Classes

ClassDescription
ticketThe sheet: a body over a stub, on a perforation between two notches
ticket-rowThe stub at the end of the body instead of under it, as a coupon has it. With ticket
ticket-bodyWhat the ticket is for: a raised face with 20px of padding that takes the room the stub leaves
ticket-stubWhat is checked or torn off, beyond the perforation

#Custom properties

PropertyDefaultDescription
--ticket-radiusvar(--radius-surface)The sheet's corner
--ticket-notch.5remRadius of the notch at each end of the tear line
--ticket-padding1.25remPadding of the body and the stub

#Source

@master/ui/components/ticket.css
/* * Ticket: a sheet with a tear line: a pass, a coupon, a receipt and its stub. * Two raised faces meet on a perforation: the body, and the stub that would be torn off. Where the browser * has corner-shape, the two corners of each face that lie on the tear line are scooped, so together they * punch a half-round notch into each edge of the sheet, and the contour and the shadow follow the notch * because they follow the corner. Elsewhere the faces meet square and the perforation alone parts them. * Each face is cut along the tear line, so neither draws a contour across it: the only line there is the * perforation, a row of 4px dashes between the notches. * Geometry: the surface corner, 8px notches and 20px of padding. The stub is below the body, or at its end * with ticket-row. */@layer components {  .ticket {    --ticket-radius: var(--radius-surface);    --ticket-notch: .5rem;    --ticket-padding: 1.25rem;    --surface-color: var(--color-surface-raised);    position: relative;    display: flex;    flex-direction: column;    min-width: 0;    margin: 0;    color: var(--color-text-body);    /* The stub at the end of the body, as a coupon has it. */    &.ticket-row {      flex-direction: row;      .ticket-body {        border-radius: calc(var(--ticket-radius) * var(--corner-scale));        @apply --surface-sheet;        border-start-end-radius: 0;        border-end-end-radius: 0;        clip-path: inset(-4rem 0 -4rem -4rem);        &:dir(rtl) {          clip-path: inset(-4rem -4rem -4rem 0);        }      }      .ticket-stub {        border-radius: calc(var(--ticket-radius) * var(--corner-scale));        border-start-start-radius: 0;        border-end-start-radius: 0;        clip-path: inset(-4rem -4rem -4rem 0);        &:dir(rtl) {          clip-path: inset(-4rem 0 -4rem -4rem);        }        /* The perforation runs down the stub's start edge. */        &::before {          inset: calc(var(--ticket-notch) + .25rem) auto calc(var(--ticket-notch) + .25rem) 0;          width: 1px;          height: auto;          background: repeating-linear-gradient(180deg, var(--color-line-strong) 0 4px, transparent 0 8px);        }        &:dir(rtl)::before {          inset-inline: 0 auto;        }      }    }    @supports (corner-shape: scoop) {      .ticket-body {        border-end-start-radius: var(--ticket-notch);        border-end-end-radius: var(--ticket-notch);        corner-end-start-shape: scoop;        corner-end-end-shape: scoop;      }      .ticket-stub {        border-start-start-radius: var(--ticket-notch);        border-start-end-radius: var(--ticket-notch);        corner-start-start-shape: scoop;        corner-start-end-shape: scoop;      }      &.ticket-row {        .ticket-body {          border-end-start-radius: calc(var(--ticket-radius) * var(--corner-scale));          border-start-end-radius: var(--ticket-notch);          border-end-end-radius: var(--ticket-notch);          corner-end-start-shape: var(--corner-shape);          corner-start-end-shape: scoop;          corner-end-end-shape: scoop;        }        .ticket-stub {          border-start-end-radius: calc(var(--ticket-radius) * var(--corner-scale));          border-start-start-radius: var(--ticket-notch);          border-end-start-radius: var(--ticket-notch);          corner-start-end-shape: var(--corner-shape);          corner-start-start-shape: scoop;          corner-end-start-shape: scoop;        }      }    }  }  /* The two faces. Each is a raised sheet with a corner of its own, cut straight where it meets the other. */  .ticket-body, .ticket-stub {    position: relative;    min-width: 0;    padding: var(--ticket-padding);    border-radius: calc(var(--ticket-radius) * var(--corner-scale));    corner-shape: var(--corner-shape);    @apply --surface-sheet;    box-shadow: var(--shadow-raised);    @media (forced-colors: active) {      outline: 1px solid CanvasText;      outline-offset: -1px;    }  }  /*   * What the ticket is for. It takes the room the stub leaves. Over a stub it is one even tone and the stub   * carries the fall of light, so the two read as one sheet from top to foot.   */  .ticket-body {    flex: 1 1 auto;    background: var(--color-surface-raised);    border-end-start-radius: 0;    border-end-end-radius: 0;    clip-path: inset(-4rem -4rem 0 -4rem);  }  /* What is torn off: a code, a price, a seat. */  .ticket-stub {    flex: none;    border-start-start-radius: 0;    border-start-end-radius: 0;    clip-path: inset(0 -4rem -4rem -4rem);    /* The perforation, between the notches. */    &::before {      content: '';      position: absolute;      inset: 0 calc(var(--ticket-notch) + .25rem) auto;      height: 1px;      background: repeating-linear-gradient(90deg, var(--color-line-strong) 0 4px, transparent 0 8px);      pointer-events: none;    }    @media (forced-colors: active) {      &::before {        background: CanvasText;      }    }  }}