Components
Ticket
An event/boarding ticket, either a paper stub or a modern boarding-pass card. Purely presentational — no Morphos equivalent.
Installation
npx kosmesis add ticketpnpm dlx kosmesis add ticketyarn dlx kosmesis add ticketbunx kosmesis add ticketInstall the following dependencies:
npm install @morphos/iconspnpm add @morphos/iconsyarn add @morphos/iconsbun add @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";export type TicketKind = "stub" | "boarding-pass" | "raffle";export type TicketVariant = "physical" | "modern";export interface TicketDetail { label: string; value: string;}export interface TicketProps { kind?: TicketKind; variant?: TicketVariant; title: string; subtitle?: string; date: string; time?: string; code: string; details?: TicketDetail[]; /** Origin/destination airport codes — `"boarding-pass"` only. */ origin?: string; destination?: string; class?: string;}@Component()export class Ticket extends StatelessComponent<TicketProps> { private renderDetailGrid(physical: boolean) { const { date, time, details } = this.props; const label = physical ? "text-[10px] tracking-wide text-neutral-500 uppercase" : "text-[10px] tracking-wide text-muted-foreground uppercase"; const value = physical ? "font-mono text-sm font-semibold" : "text-sm font-medium"; return ( <div class="mt-1 flex flex-wrap gap-4"> <div class="flex flex-col"> <span class={label}>Date</span> <span class={value}>{date}</span> </div> {time && ( <div class="flex flex-col"> <span class={label}>Time</span> <span class={value}>{time}</span> </div> )} {details?.map((detail, i) => ( <div key={`${detail.label}-${String(i)}`} class="flex flex-col"> <span class={label}>{detail.label}</span> <span class={value}>{detail.value}</span> </div> ))} </div> ); } private renderRouteHero(physical: boolean) { const { origin, destination } = this.props; return ( <div class={cn("flex items-center gap-3 font-mono font-bold", physical ? "text-2xl text-neutral-900" : "text-2xl text-foreground")}> <span>{origin}</span> <Icon name="Plane" size={18} class={physical ? "text-neutral-400" : "text-muted-foreground"} /> <span>{destination}</span> </div> ); } private renderStub(physical: boolean) { const { code } = this.props; if (physical) { return ( <div class="flex w-24 shrink-0 flex-col items-center justify-between gap-3 border-l border-dashed border-neutral-400 bg-amber-100/70 p-3"> <Icon name="Ticket" size={14} class="mt-1 text-neutral-500" /> <div class="h-16 w-full bg-[repeating-linear-gradient(90deg,black_0px,black_2px,transparent_2px,transparent_4px)]" /> <span class="font-mono text-[10px] tracking-widest text-neutral-600">{code}</span> </div> ); } return ( <div class="flex w-24 shrink-0 flex-col items-center justify-center gap-2 border-l border-dashed border-border bg-muted/40 p-3"> <Icon name="QrCode" size={28} class="text-foreground" /> <span class="font-mono text-[10px] tracking-widest text-muted-foreground">{code}</span> </div> ); } private renderStubBody(physical: boolean) { const { kind, title, subtitle } = this.props; return ( <div class="flex flex-1 flex-col gap-3 p-5"> {kind === "boarding-pass" ? ( this.renderRouteHero(physical) ) : physical ? ( <span class="text-base font-bold">{title}</span> ) : ( <div class="flex items-center gap-2"> <span class="flex size-8 shrink-0 items-center justify-center rounded-full bg-primary/10 text-primary"> <Icon name="Ticket" size={16} /> </span> <span class="text-base font-semibold">{title}</span> </div> )} {kind === "boarding-pass" && <span class={cn("font-medium", physical ? "text-sm text-neutral-700" : "text-sm text-foreground")}>{title}</span>} {subtitle && <span class={physical ? "text-xs text-neutral-600" : "text-sm text-muted-foreground"}>{subtitle}</span>} {this.renderDetailGrid(physical)} </div> ); } private renderStubLayout(physical: boolean) { const { class: cls } = this.props; return ( <div data-slot="ticket" data-variant={physical ? "physical" : "modern"} class={cn( "relative flex w-full max-w-md", physical ? "bg-amber-50 text-neutral-900 shadow-md" : "overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs", cls, )} > {this.renderStubBody(physical)} {this.renderStub(physical)} {physical && ( <> <div class="absolute -top-3 right-24 h-6 w-6 translate-x-1/2 rounded-full bg-background" /> <div class="absolute -bottom-3 right-24 h-6 w-6 translate-x-1/2 rounded-full bg-background" /> </> )} </div> ); } private renderRaffleLayout(physical: boolean) { const { title, subtitle, date, code, class: cls } = this.props; return ( <div data-slot="ticket" data-variant={physical ? "physical" : "modern"} class={cn( "relative mx-auto flex w-60 flex-col", physical ? "bg-amber-50 text-neutral-900 shadow-md" : "overflow-hidden rounded-2xl border bg-card text-card-foreground shadow-xs", cls, )} > <div class="flex flex-col items-center gap-1 px-4 pt-5 pb-6 text-center"> <span class={cn("font-semibold", physical ? "text-sm" : "text-sm text-foreground")}>{title}</span> {subtitle && <span class={physical ? "text-xs text-neutral-600" : "text-xs text-muted-foreground"}>{subtitle}</span>} <span class={cn("mt-2 font-mono text-2xl font-bold tracking-wider", !physical && "text-foreground")}>{code}</span> <span class={physical ? "text-[11px] text-neutral-500" : "text-[11px] text-muted-foreground"}>{date}</span> </div> <div class={cn("relative border-t border-dashed", physical ? "border-neutral-400" : "border-border")}> {physical && ( <> <div class="absolute top-1/2 -left-3 h-6 w-6 -translate-y-1/2 rounded-full bg-background" /> <div class="absolute top-1/2 -right-3 h-6 w-6 -translate-y-1/2 rounded-full bg-background" /> </> )} </div> <div class={cn("flex items-center justify-center px-4 py-3", physical ? "bg-amber-100/70" : "bg-muted/40")}> <span class={cn("font-mono text-sm font-bold tracking-wider", physical ? "text-neutral-700" : "text-muted-foreground")}>{`No. ${code}`}</span> </div> </div> ); } render() { const { kind = "stub", variant } = this.props; const physical = variant === "physical"; return kind === "raffle" ? this.renderRaffleLayout(physical) : this.renderStubLayout(physical); }}Install the following dependencies:
npm install @praxisjs/css @morphos/iconspnpm add @praxisjs/css @morphos/iconsyarn add @praxisjs/css @morphos/iconsbun add @praxisjs/css @morphos/iconsCopy and paste the following code into your project.
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class TicketStyles extends Stylesheet { $stubShellPhysical = this.css({ position: "relative", display: "flex", width: "100%", maxWidth: "28rem", backgroundColor: "oklch(0.98 0.02 85)", color: "oklch(0.2 0 0)", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", }); $stubShellModern = this.css({ display: "flex", width: "100%", maxWidth: "28rem", overflow: "hidden", borderRadius: "1rem", border: `1px solid ${t.border}`, backgroundColor: t.card, color: t.cardForeground, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $main = this.css({ display: "flex", flex: "1 1 0%", flexDirection: "column", gap: "0.75rem", padding: "1.25rem" }); $titlePhysical = this.css({ fontSize: "1rem", fontWeight: 700 }); $subtitlePhysical = this.css({ fontSize: "0.75rem", color: "oklch(0.45 0 0)" }); $headerModernRow = this.css({ display: "flex", alignItems: "center", gap: "0.5rem" }); $icon = this.css({ display: "flex", height: "2rem", width: "2rem", flexShrink: 0, alignItems: "center", justifyContent: "center", borderRadius: "9999px", backgroundColor: `color-mix(in oklab, ${t.primary} 10%, transparent)`, color: t.primary, }); $titleModern = this.css({ fontSize: "1rem", fontWeight: 600 }); $subtitleModern = this.css({ fontSize: "0.875rem", color: t.mutedForeground }); $flightNumberPhysical = this.css({ fontSize: "0.875rem", fontWeight: 500, color: "oklch(0.35 0 0)" }); $flightNumberModern = this.css({ fontSize: "0.875rem", fontWeight: 500, color: t.foreground }); $routeHero = this.css({ display: "flex", alignItems: "center", gap: "0.75rem", fontFamily: "ui-monospace, monospace", fontWeight: 700, fontSize: "1.5rem" }); $routeHeroPhysical = this.css({ color: "oklch(0.2 0 0)" }); $routeHeroModern = this.css({ color: t.foreground }); $planeIconPhysical = this.css({ color: "oklch(0.65 0 0)" }); $planeIconModern = this.css({ color: t.mutedForeground }); $detailRow = this.css({ marginTop: "0.25rem", display: "flex", flexWrap: "wrap", gap: "1rem" }); $detail = this.css({ display: "flex", flexDirection: "column" }); $detailLabelPhysical = this.css({ fontSize: "10px", letterSpacing: "0.05em", color: "oklch(0.55 0 0)", textTransform: "uppercase" }); $detailLabelModern = this.css({ fontSize: "10px", letterSpacing: "0.05em", color: t.mutedForeground, textTransform: "uppercase" }); $detailValuePhysical = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "0.875rem", fontWeight: 600 }); $detailValueModern = this.css({ fontSize: "0.875rem", fontWeight: 500 }); $stubPhysical = this.css({ display: "flex", width: "6rem", flexShrink: 0, flexDirection: "column", alignItems: "center", justifyContent: "space-between", gap: "0.75rem", borderLeft: "1px dashed oklch(0.7 0.02 85)", backgroundColor: "oklch(0.94 0.03 85)", padding: "0.75rem", }); $stubModern = this.css({ display: "flex", width: "6rem", flexShrink: 0, flexDirection: "column", alignItems: "center", justifyContent: "center", gap: "0.5rem", borderLeft: `1px dashed ${t.border}`, backgroundColor: `color-mix(in oklab, ${t.muted} 40%, transparent)`, padding: "0.75rem", }); $stubIcon = this.css({ marginTop: "0.25rem", color: "oklch(0.55 0 0)" }); $barcode = this.css({ height: "4rem", width: "100%", backgroundImage: "repeating-linear-gradient(90deg, black 0px, black 2px, transparent 2px, transparent 4px)", }); $codePhysical = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "10px", letterSpacing: "0.15em", color: "oklch(0.45 0 0)" }); $codeModern = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "10px", letterSpacing: "0.15em", color: t.mutedForeground }); $notchTop = this.css({ position: "absolute", top: "-0.75rem", right: "6rem", height: "1.5rem", width: "1.5rem", transform: "translateX(50%)", borderRadius: "9999px", backgroundColor: t.background, }); $notchBottom = this.css({ position: "absolute", bottom: "-0.75rem", right: "6rem", height: "1.5rem", width: "1.5rem", transform: "translateX(50%)", borderRadius: "9999px", backgroundColor: t.background, }); $raffleShellPhysical = this.css({ position: "relative", margin: "0 auto", display: "flex", width: "15rem", flexDirection: "column", backgroundColor: "oklch(0.98 0.02 85)", color: "oklch(0.2 0 0)", boxShadow: "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)", }); $raffleShellModern = this.css({ position: "relative", margin: "0 auto", display: "flex", width: "15rem", flexDirection: "column", overflow: "hidden", borderRadius: "1rem", border: `1px solid ${t.border}`, backgroundColor: t.card, color: t.cardForeground, boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", }); $raffleTop = this.css({ display: "flex", flexDirection: "column", alignItems: "center", gap: "0.25rem", padding: "1.25rem 1rem 1.5rem", textAlign: "center", }); $raffleTitlePhysical = this.css({ fontSize: "0.875rem", fontWeight: 600 }); $raffleTitleModern = this.css({ fontSize: "0.875rem", fontWeight: 600, color: t.foreground }); $raffleSubtitlePhysical = this.css({ fontSize: "0.75rem", color: "oklch(0.45 0 0)" }); $raffleSubtitleModern = this.css({ fontSize: "0.75rem", color: t.mutedForeground }); $raffleCode = this.css({ marginTop: "0.5rem", fontFamily: "ui-monospace, monospace", fontSize: "1.5rem", fontWeight: 700, letterSpacing: "0.05em" }); $raffleCodeModern = this.css({ color: t.foreground }); $raffleDatePhysical = this.css({ fontSize: "11px", color: "oklch(0.55 0 0)" }); $raffleDateModern = this.css({ fontSize: "11px", color: t.mutedForeground }); $raffleSeamPhysical = this.css({ position: "relative", borderTop: "1px dashed oklch(0.7 0.02 85)" }); $raffleSeamModern = this.css({ position: "relative", borderTop: `1px dashed ${t.border}` }); $notchLeft = this.css({ position: "absolute", top: "50%", left: "-0.75rem", height: "1.5rem", width: "1.5rem", transform: "translateY(-50%)", borderRadius: "9999px", backgroundColor: t.background, }); $notchRight = this.css({ position: "absolute", top: "50%", right: "-0.75rem", height: "1.5rem", width: "1.5rem", transform: "translateY(-50%)", borderRadius: "9999px", backgroundColor: t.background, }); $raffleStubPhysical = this.css({ display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "oklch(0.94 0.03 85)", padding: "0.75rem 1rem", }); $raffleStubModern = this.css({ display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: `color-mix(in oklab, ${t.muted} 40%, transparent)`, padding: "0.75rem 1rem", }); $raffleStubTextPhysical = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "0.875rem", fontWeight: 700, letterSpacing: "0.05em", color: "oklch(0.35 0 0)" }); $raffleStubTextModern = this.css({ fontFamily: "ui-monospace, monospace", fontSize: "0.875rem", fontWeight: 700, letterSpacing: "0.05em", color: t.mutedForeground });}export type TicketKind = "stub" | "boarding-pass" | "raffle";export type TicketVariant = "physical" | "modern";export interface TicketDetail { label: string; value: string;}export interface TicketProps { kind?: TicketKind; variant?: TicketVariant; title: string; subtitle?: string; date: string; time?: string; code: string; details?: TicketDetail[]; /** Origin/destination airport codes — `"boarding-pass"` only. */ origin?: string; destination?: string; class?: string;}@Component()export class Ticket extends StatelessComponent<TicketProps> { @Styled(TicketStyles) $s!: TicketStyles; private renderDetailGrid(physical: boolean) { const { date, time, details } = this.props; const labelClass = physical ? this.$s.$detailLabelPhysical : this.$s.$detailLabelModern; const valueClass = physical ? this.$s.$detailValuePhysical : this.$s.$detailValueModern; return ( <div class={this.$s.$detailRow}> <div class={this.$s.$detail}> <span class={labelClass}>Date</span> <span class={valueClass}>{date}</span> </div> {time && ( <div class={this.$s.$detail}> <span class={labelClass}>Time</span> <span class={valueClass}>{time}</span> </div> )} {details?.map((detail, i) => ( <div key={`${detail.label}-${String(i)}`} class={this.$s.$detail}> <span class={labelClass}>{detail.label}</span> <span class={valueClass}>{detail.value}</span> </div> ))} </div> ); } private renderRouteHero(physical: boolean) { const { origin, destination } = this.props; return ( <div class={cx(this.$s.$routeHero, physical ? this.$s.$routeHeroPhysical : this.$s.$routeHeroModern)}> <span>{origin}</span> <Icon name="Plane" size={18} class={physical ? this.$s.$planeIconPhysical : this.$s.$planeIconModern} /> <span>{destination}</span> </div> ); } private renderStub(physical: boolean) { const { code } = this.props; if (physical) { return ( <div class={this.$s.$stubPhysical}> <Icon name="Ticket" size={14} class={this.$s.$stubIcon} /> <div class={this.$s.$barcode} /> <span class={this.$s.$codePhysical}>{code}</span> </div> ); } return ( <div class={this.$s.$stubModern}> <Icon name="QrCode" size={28} /> <span class={this.$s.$codeModern}>{code}</span> </div> ); } private renderStubBody(physical: boolean) { const { kind, title, subtitle } = this.props; return ( <div class={this.$s.$main}> {kind === "boarding-pass" ? ( this.renderRouteHero(physical) ) : physical ? ( <span class={this.$s.$titlePhysical}>{title}</span> ) : ( <div class={this.$s.$headerModernRow}> <span class={this.$s.$icon}> <Icon name="Ticket" size={16} /> </span> <span class={this.$s.$titleModern}>{title}</span> </div> )} {kind === "boarding-pass" && <span class={physical ? this.$s.$flightNumberPhysical : this.$s.$flightNumberModern}>{title}</span>} {subtitle && <span class={physical ? this.$s.$subtitlePhysical : this.$s.$subtitleModern}>{subtitle}</span>} {this.renderDetailGrid(physical)} </div> ); } private renderStubLayout(physical: boolean) { const { class: cls } = this.props; return ( <div data-slot="ticket" data-variant={physical ? "physical" : "modern"} class={cx(physical ? this.$s.$stubShellPhysical : this.$s.$stubShellModern, cls)}> {this.renderStubBody(physical)} {this.renderStub(physical)} {physical && ( <> <div class={this.$s.$notchTop} /> <div class={this.$s.$notchBottom} /> </> )} </div> ); } private renderRaffleLayout(physical: boolean) { const { title, subtitle, date, code, class: cls } = this.props; return ( <div data-slot="ticket" data-variant={physical ? "physical" : "modern"} class={cx(physical ? this.$s.$raffleShellPhysical : this.$s.$raffleShellModern, cls)}> <div class={this.$s.$raffleTop}> <span class={physical ? this.$s.$raffleTitlePhysical : this.$s.$raffleTitleModern}>{title}</span> {subtitle && <span class={physical ? this.$s.$raffleSubtitlePhysical : this.$s.$raffleSubtitleModern}>{subtitle}</span>} <span class={cx(this.$s.$raffleCode, !physical && this.$s.$raffleCodeModern)}>{code}</span> <span class={physical ? this.$s.$raffleDatePhysical : this.$s.$raffleDateModern}>{date}</span> </div> <div class={physical ? this.$s.$raffleSeamPhysical : this.$s.$raffleSeamModern}> {physical && ( <> <div class={this.$s.$notchLeft} /> <div class={this.$s.$notchRight} /> </> )} </div> <div class={physical ? this.$s.$raffleStubPhysical : this.$s.$raffleStubModern}> <span class={physical ? this.$s.$raffleStubTextPhysical : this.$s.$raffleStubTextModern}>{`No. ${code}`}</span> </div> </div> ); } render() { const { kind = "stub", variant } = this.props; const physical = variant === "physical"; return kind === "raffle" ? this.renderRaffleLayout(physical) : this.renderStubLayout(physical); }}Examples
Physical
Kind: Boarding Pass
Kind: Raffle
About
Purely presentational — no Morphos equivalent. Two independent props control the result:
variantswaps the chrome rather than just colors:"modern"(default) — a theme-aware boarding-pass card built from the same tokens as the rest of the registry, with a plain dashed divider separating the main stub from a QR-code stub."physical"— a paper ticket. It uses fixed cardstock colors instead of theme tokens and punches circular notches through the edge exactly where the dashed perforation meets it — the classic torn-stub silhouette. The notches are circles colored with the background token/class, so they read correctly as long as the ticket sits on the page background, the same assumption every card-on-canvas surface in this registry already makes.
kindswaps the layout:"stub"(default) — a general-admission ticket: main content on the left, a barcode/QR stub on the right, seam running top-to-bottom."boarding-pass"— the same shell, but the header is replaced by anorigin/destinationroute hero with a plane icon between the two airport codes."raffle"— a narrow ticket split vertically instead of horizontally: notches on the left/right of a single dashed seam, the samecodeprinted on both the kept half and the drawn stub below it.
details is an open-ended list of extra label/value pairs (seat, gate, section, ...) rendered
alongside the built-in date/time fields. origin/destination only apply to
kind="boarding-pass".
Usage
import { Ticket } from "@/components/ui/ticket";
<Ticket
title="Signal & Noise"
subtitle="The Grand Hall, Lisbon"
date="Aug 03, 2026"
time="20:00"
code="KSM-88213"
details={[
{ label: "Seat", value: "12A" },
{ label: "Gate", value: "B7" },
]}
/>
// Boarding pass — same shell, a route hero instead of a plain title
<Ticket
kind="boarding-pass"
origin="LIS"
destination="JFK"
title="TAP 1954 · Ada Lovelace"
date="Aug 03, 2026"
time="Boarding 19:40"
code="KSM-88213"
details={[{ label: "Gate", value: "22" }, { label: "Seat", value: "14C" }]}
/>
// Raffle — split vertically instead of horizontally
<Ticket variant="physical" kind="raffle" title="Summer Raffle" subtitle="Kosmesis Café" date="Aug 03, 2026" code="0427" />Props
| Prop | Type | Default |
|---|---|---|
title | string | — |
subtitle | string | — |
date | string | — |
time | string | — |
code | string | — |
details | { label: string; value: string }[] | — |
origin | string | — |
destination | string | — |
kind | "stub" | "boarding-pass" | "raffle" | "stub" |
variant | "physical" | "modern" | "modern" |
class | string | — |