Signature Library · Indian
Bus Ticket Roll
Pick a fare stage: the conductor's punch makes a hole and the fare changes.
Sends a change event with the picked fare.
States
Default
Hover
Active
Focus
Code
bus-ticket.html
<div class="sig-bus-ticket" role="group" aria-label="City Bus Service">
<span class="sig-bus-ticket__stages">
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 1, ₹5" data-fare="5">1</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 2, ₹8" data-fare="8">2</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 3, ₹10" data-fare="10">3</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="true" aria-label="Stage 4, ₹12" data-fare="12">4</button>
</span>
<span class="sig-bus-ticket__mid">
<span class="sig-bus-ticket__title">City Bus Service</span>
<span class="sig-bus-ticket__serial">No. A 048213</span>
<span class="sig-bus-ticket__total" data-total aria-live="polite">₹12</span>
<span class="sig-bus-ticket__small">Keep till the end</span>
</span>
<span class="sig-bus-ticket__stages">
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 5, ₹15" data-fare="15">5</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 6, ₹20" data-fare="20">6</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 7, ₹25" data-fare="25">7</button>
<button class="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 8, ₹30" data-fare="30">8</button>
</span>
</div>
bus-ticket.css
.sig-bus-ticket {
--sig-paper: #f6c9d3;
--sig-ink: #2a2340;
--sig-speed: 140ms;
}
.sig-bus-ticket {
position: relative;
box-sizing: border-box;
display: grid;
grid-template-columns: 26px 1fr 26px;
gap: 4px;
width: 196px;
padding: 16px 8px;
color: var(--sig-ink);
background: var(--sig-paper);
font-family: system-ui, sans-serif;
transform: rotate(-2deg);
mask: radial-gradient(circle 4px at 7px 0, transparent 96%, #000) 0 0 / 14px 51% repeat-x, radial-gradient(circle 4px at 7px 100%, transparent 96%, #000) 0 100% / 14px 51% repeat-x;
}
.sig-bus-ticket__stages {
display: grid;
gap: 3px;
}
.sig-bus-ticket__stage {
width: 26px;
height: 26px;
padding: 0;
color: inherit;
font-family: ui-monospace, Consolas, monospace;
font-size: 11px;
font-weight: 700;
background: transparent;
border: 1px solid color-mix(in srgb, var(--sig-ink) 45%, transparent);
cursor: pointer;
transition: transform var(--sig-speed) ease;
}
.sig-bus-ticket__stage:hover {
transform: scale(1.12);
}
.sig-bus-ticket__stage:focus-visible {
outline: 2px solid var(--sig-ink);
outline-offset: 1px;
}
.sig-bus-ticket__stage[aria-pressed=true] {
color: transparent;
background: radial-gradient(circle 8px, #15151b 96%, transparent);
border-color: transparent;
}
.sig-bus-ticket__mid {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 4px;
min-width: 0;
text-align: center;
border-inline: 1px dashed color-mix(in srgb, var(--sig-ink) 45%, transparent);
}
.sig-bus-ticket__title {
font-size: 11px;
font-weight: 800;
line-height: 1.15;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.sig-bus-ticket__serial {
font-family: ui-monospace, Consolas, monospace;
font-size: 10px;
color: #b3242a;
}
.sig-bus-ticket__total {
font-family: Georgia, 'Times New Roman', serif;
font-size: 34px;
font-weight: 700;
line-height: 1;
}
.sig-bus-ticket__small {
font-size: 8px;
opacity: 0.7;
}
@media (prefers-reduced-motion: reduce) {
.sig-bus-ticket__stage {
animation: none !important;
transition: none !important;
}
}
bus-ticket.js
document.querySelectorAll(".sig-bus-ticket").forEach((el) => {
const total = el.querySelector("[data-total]");
const stages = Array.from(el.querySelectorAll("[data-fare]"));
const punch = (e) => {
const hit = (e.target).closest("[data-fare]");
if (!hit) return;
stages.forEach((s) => s.setAttribute("aria-pressed", String(s === hit)));
total.textContent = "₹" + hit.dataset.fare;
el.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: hit.dataset.fare }));
};
el.addEventListener("click", punch);
return () => el.removeEventListener("click", punch);
});
bus-ticket.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./bus-ticket.css"; // the CSS tab
function attach(el: HTMLElement) {
const total = el.querySelector("[data-total]") as HTMLElement;
const stages = Array.from(el.querySelectorAll("[data-fare]")) as HTMLElement[];
const punch = (e: MouseEvent) => {
const hit = (e.target as HTMLElement).closest("[data-fare]") as HTMLElement;
if (!hit) return;
stages.forEach((s) => s.setAttribute("aria-pressed", String(s === hit)));
total.textContent = "₹" + hit.dataset.fare;
el.dispatchEvent(new CustomEvent("change", { bubbles: true, detail: hit.dataset.fare }));
};
el.addEventListener("click", punch);
return () => el.removeEventListener("click", punch);
}
export function SigBusTicket({ children = "City Bus Service", ...props }: React.ComponentProps<"div">) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<div className="sig-bus-ticket" role="group" aria-label="City Bus Service" ref={ref} {...props}>
<span className="sig-bus-ticket__stages">
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 1, ₹5" data-fare="5">{"1"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 2, ₹8" data-fare="8">{"2"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 3, ₹10" data-fare="10">{"3"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="true" aria-label="Stage 4, ₹12" data-fare="12">{"4"}</button>
</span>
<span className="sig-bus-ticket__mid">
<span className="sig-bus-ticket__title">{children}</span>
<span className="sig-bus-ticket__serial">{"No. A 048213"}</span>
<span className="sig-bus-ticket__total" data-total aria-live="polite">{"₹12"}</span>
<span className="sig-bus-ticket__small">{"Keep till the end"}</span>
</span>
<span className="sig-bus-ticket__stages">
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 5, ₹15" data-fare="15">{"5"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 6, ₹20" data-fare="20">{"6"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 7, ₹25" data-fare="25">{"7"}</button>
<button className="sig-bus-ticket__stage" type="button" aria-pressed="false" aria-label="Stage 8, ₹30" data-fare="30">{"8"}</button>
</span>
</div>
);
}
bus-ticket.html
<div class="sig-bus-ticket relative [box-sizing:border-box] grid [grid-template-columns:26px_1fr_26px] gap-[4px] w-[196px] [padding:16px_8px] text-[#2a2340] bg-[#f6c9d3] [font-family:system-ui,_sans-serif] [transform:rotate(-2deg)] [mask:radial-gradient(circle_4px_at_7px_0,_transparent_96%,_#000)_0_0_/_14px_51%_repeat-x,_radial-gradient(circle_4px_at_7px_100%,_transparent_96%,_#000)_0_100%_/_14px_51%_repeat-x]" role="group" aria-label="City Bus Service">
<span class="grid gap-[3px]">
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 1, ₹5" data-fare="5">1</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 2, ₹8" data-fare="8">2</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 3, ₹10" data-fare="10">3</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="true" aria-label="Stage 4, ₹12" data-fare="12">4</button>
</span>
<span class="[display:flex] [flex-direction:column] items-center justify-center gap-[4px] [min-width:0] [text-align:center] [border-inline:1px_dashed_color-mix(in_srgb,_#2a2340_45%,_transparent)]">
<span class="text-[11px] font-[800] [line-height:1.15] [letter-spacing:0.06em] [text-transform:uppercase]">City Bus Service</span>
<span class="[font-family:ui-monospace,_Consolas,_monospace] text-[10px] text-[#b3242a]">No. A 048213</span>
<span class="[font-family:Georgia,_'Times_New_Roman',_serif] text-[34px] font-[700] [line-height:1]" data-total aria-live="polite">₹12</span>
<span class="text-[8px] [opacity:0.7]">Keep till the end</span>
</span>
<span class="grid gap-[3px]">
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 5, ₹15" data-fare="15">5</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 6, ₹20" data-fare="20">6</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 7, ₹25" data-fare="25">7</button>
<button class="w-[26px] h-[26px] [padding:0] [color:inherit] [font-family:ui-monospace,_Consolas,_monospace] text-[11px] font-[700] [background:transparent] [border:1px_solid_color-mix(in_srgb,_#2a2340_45%,_transparent)] cursor-pointer [transition:transform_140ms_ease] hover:[transform:scale(1.12)] focus-visible:[outline:2px_solid_#2a2340] focus-visible:[outline-offset:1px] [&[aria-pressed=true]]:[color:transparent] [&[aria-pressed=true]]:[background:radial-gradient(circle_8px,_#15151b_96%,_transparent)] [&[aria-pressed=true]]:[border-color:transparent] motion-reduce:[transition:none!important]" type="button" aria-pressed="false" aria-label="Stage 8, ₹30" data-fare="30">8</button>
</span>
</div>
<!-- Needs the script from the JS tab. -->