Golden Passport Design System — React component library built on Tailwind CSS v4, Headless UI v2, and Lucide icons.
- Install
- Setup
- Running Storybook
- Usage examples
- Theming & token overrides
- Design tokens (JS)
- Stories
- Build & publishing
pnpm add @golden-passport/ds @headlessui/react @floating-ui/react lucide-reactPeer dependencies — install in your app if not already present:
pnpm add react react-dom tailwindcssRequires React ≥ 18, Tailwind CSS ≥ 4, Headless UI ≥ 2, Floating UI ≥ 0.26.
In your app's root CSS file (processed by Tailwind):
/* app/globals.css —or— src/index.css */
@import "@golden-passport/ds/styles";This gives you:
- All colour scales (
primary-*,ink-*,slate-*) as Tailwind utilities - Typography (
font-display,font-body,font-mono) - Radius, shadow, and animation tokens
- Class-based dark mode (
dark:variant driven by.darkon<html>) - Utility classes:
gp-focus,gp-ai-dot
// app/layout.tsx
import '@golden-passport/ds/styles';// src/main.tsx
import '@golden-passport/ds/styles';<link
href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@600;700;800&family=DM+Sans:opsz,wght@9..40,400;9..40,500;9..40,600&family=JetBrains+Mono:wght@400;500&display=swap"
rel="stylesheet"
/>Or self-host via Fontsource:
pnpm add @fontsource/plus-jakarta-sans @fontsource/dm-sans @fontsource/jetbrains-monoimport '@fontsource/plus-jakarta-sans/600.css';
import '@fontsource/plus-jakarta-sans/700.css';
import '@fontsource/plus-jakarta-sans/800.css';
import '@fontsource/dm-sans/400.css';
import '@fontsource/dm-sans/500.css';
import '@fontsource/dm-sans/600.css';
import '@fontsource/jetbrains-mono/400.css';Dark mode is class-based — toggle the dark class on <html>:
document.documentElement.classList.toggle('dark', isDarkMode);Storybook is the primary way to browse, develop, and test components.
pnpm storybookOpens at http://localhost:6006. Supports:
- Hot reload — changes to components and stories update instantly
- Light / Dark theme toggle via the toolbar globe icon
- Viewport presets — mobile (
375 px), tablet, desktop - Autodocs — auto-generated API docs for every component
pnpm build-storybook
# outputs to storybook-static/ — deploy to any static hostEvery story is covered by interaction tests (play functions) and WCAG 2.x accessibility scans (axe-core). Tests run in a headless Chromium browser via Vitest + Playwright.
pnpm test-storybook:light # light mode (487 tests)
pnpm test-storybook:dark # dark mode (487 tests, separate Vitest config)
pnpm test-storybook:all # both passes — full gate
pnpm test-storybook:coverage # light mode with V8 coverage reportWhy two separate commands?
The Storybook GUI's ▷ "Run component tests" button always uses vitest.config.ts, which renders stories in light mode. There is no built-in way to make the GUI runner switch configs. Dark-mode testing requires vitest.config.dark.ts, which injects STORYBOOK_THEME=Dark at the Vite build level so every story renders with the .dark class applied before the first paint.
| Command | Renders as | Intended use |
|---|---|---|
GUI ▷ / pnpm test-storybook:light |
Light | Fast feedback during development |
pnpm test-storybook:dark |
Dark | Pre-ship or CI dark-mode a11y gate |
pnpm test-storybook:all |
Both | Full CI gate |
Note — changing the theme in the Storybook toolbar (
globals=theme:Darkin the URL) only affects the visual preview; it has no effect on what the test runner renders.
import { Button } from '@golden-passport/ds';
<Button variant="primary">Deploy</Button>
<Button variant="secondary">Save Draft</Button>
<Button variant="ghost">Cancel</Button>
<Button variant="danger">Delete</Button>
<Button size="sm" loading>Deploying…</Button>
<Button radius="pill">Pill shape</Button>import { Input } from '@golden-passport/ds';
import { Mail, Eye } from 'lucide-react';
// With left icon
<Input label="Email" type="email" icon={<Mail className="w-4 h-4" />} />
// With right action (show/hide password)
<Input
label="Password"
type={show ? 'text' : 'password'}
rightAction={
<button onClick={() => setShow(v => !v)}>
<Eye className="w-4 h-4" />
</button>
}
/>
// With error
<Input label="API Key" error="Invalid key format" />import { DatePicker, TimePicker, DateTimePicker, DateRangePicker } from '@golden-passport/ds';
<DatePicker
label="Passport expiry"
value={date}
onChange={setDate}
/>
<TimePicker
label="Departure time"
value={time} // "HH:mm"
onChange={setTime}
/>
<DateTimePicker
label="Appointment"
value={dt} // { date: Date, hour: number, minute: number }
onChange={setDt}
/>
<DateRangePicker
label="Trip dates"
value={range} // { start: Date | null, end: Date | null }
onChange={setRange}
/>import { SidebarNav } from '@golden-passport/ds';
import { LayoutDashboard, Users, Settings } from 'lucide-react';
<SidebarNav
logo={<img src="/logo.svg" alt="Brand" className="h-6" />}
groups={[{
items: [
{ label: 'Dashboard', href: '/dashboard', icon: <LayoutDashboard className="w-5 h-5" />, active: true },
{ label: 'Team', href: '/team', icon: <Users className="w-5 h-5" /> },
{ label: 'Settings', href: '/settings', icon: <Settings className="w-5 h-5" /> },
],
}]}
user={{ name: 'Tom Cook', email: 'tom@example.com' }}
/>import { StackedList, Avatar } from '@golden-passport/ds';
// Default row (leading + title + subtitle + trailing)
<StackedList
bordered
items={[
{
id: 1,
leading: <Avatar name="Alice Chen" size={40} />,
title: 'Alice Chen',
subtitle: 'alice@example.com',
trailing: 'Admin',
href: '/users/1',
},
]}
/>
// Custom row via renderItem
<StackedList
items={rows}
renderItem={(item) => (
<div className="flex items-center gap-3 py-3">
<StatusDot /> <span>{item.title}</span>
</div>
)}
/>import { RatingGroup } from '@golden-passport/ds';
import { Heart } from 'lucide-react';
// Basic 5-star
<RatingGroup value={rating} onChange={setRating} />
// Half steps
<RatingGroup value={3.5} onChange={setRating} allowHalf />
// Custom icons
<RatingGroup
value={2}
onChange={setRating}
iconEmpty={<Heart className="w-full h-full" strokeWidth={1.5} />}
iconFull={<Heart className="w-full h-full fill-current" strokeWidth={1.5} />}
/>import { Hyperlink } from '@golden-passport/ds';
// Default — auto-darkens primary colour for 4.5:1 contrast on light backgrounds
<Hyperlink href="/docs">Read the docs</Hyperlink>
// Variants
<Hyperlink href="/author" variant="muted">Alex Johnson</Hyperlink>
<Hyperlink href="/delete" variant="danger">Delete account</Hyperlink>
// External link (adds icon + rel="noopener noreferrer")
<Hyperlink href="https://example.com" external>Open site</Hyperlink>
// Always underlined
<Hyperlink href="/terms" underline="always">Terms of Service</Hyperlink>
// Override contrast for a white-background container
<div style={{ '--link-primary': 'var(--color-primary-500)' }}>
<Hyperlink href="#">Bright gold on white</Hyperlink>
</div>import { Dialog, Button } from '@golden-passport/ds';
<Button onClick={() => setOpen(true)}>Delete</Button>
<Dialog open={open} onClose={setOpen} title="Confirm Delete" size="sm">
<p className="text-sm text-ink-500">This action cannot be undone.</p>
<div className="flex justify-end gap-2 mt-6">
<Button variant="ghost" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="danger" onClick={confirm}>Delete</Button>
</div>
</Dialog>import { Tooltip, Button } from '@golden-passport/ds';
<Tooltip content="Deploy to production" placement="top">
<Button>Deploy</Button>
</Tooltip>All design tokens are CSS custom properties inside Tailwind's @theme {} block. Override any token after the import — no rebuilds needed.
/* your-app/src/index.css */
@import "@golden-passport/ds/styles";
@theme {
/* Swap primary brand colour */
--color-primary-400: #60A5FA;
--color-primary-500: #3B82F6;
--color-primary-600: #2563EB;
/* Replace warm neutrals with cool grays */
--color-ink-50: #F8FAFC;
--color-ink-900: #0F172A;
/* Change fonts */
--font-display: 'Inter', sans-serif;
--font-body: 'Inter', sans-serif;
/* Tighten border radius */
--radius-xl: 0.5rem;
--radius-2xl: 0.75rem;
}Every component references these variables — the override propagates automatically.
For use in SVG, canvas, style props, or non-Tailwind contexts:
import { tokens } from '@golden-passport/ds';
tokens.colors.primary500 // '#F5C200'
tokens.colors.ink900 // '#0E0D0B'
tokens.fonts.display // "'Plus Jakarta Sans', sans-serif"
tokens.radius.xl // '1rem'
tokens.shadow.md // '0 4px 12px 0 rgb(14 13 11 / 0.08)…'All stories are browsable in Storybook. Each entry has a Playground story with interactive controls, individual variant stories, and an All Variants overview. The Headless UI column indicates which primitives the component delegates to.
| Category | Name | Headless UI | Description |
|---|---|---|---|
| Styling | Colors | — | Full primary, ink, and slate colour palette swatches with hex values |
| Styling | General | — | Design token reference — spacing, radius, shadow, animation |
| Styling | Typography | — | Type scale, font families (display / body / mono), and weight samples |
| Elements | Avatar | — | Gold-gradient initials avatar, configurable size |
| Elements | Badge | — | Status pills: active, running, pending, draft, failed, ai, warning, neutral; dismissible variant |
| Elements | Button | — | Primary / secondary / ghost / danger · sm / md / lg · loading spinner · pill / square radius |
| Elements | ButtonGroup | — | Segmented button row sharing a border |
| Elements | Hyperlink | — | Styled <a> with adaptive --link-primary contrast, muted / danger variants, underline control, external-link icon |
| Elements | Menu | Menu |
Action dropdown / context menu with optional dividers and destructive items |
| Forms | ActionPanel | — | Call-to-action panel with title, body, and primary / secondary actions |
| Forms | Checkbox | — | Labelled checkbox with indeterminate support |
| Forms | DatePicker | Popover |
Calendar popover date picker — Clear / Today shortcuts |
| Forms | DateRangePicker | Popover |
Two-field From / To range picker with hover-preview strip |
| Forms | DateTimePicker | Popover |
Combined date + time picker in a single popover |
| Forms | Fieldset | — | Form layout primitives: Fieldset, Legend, FieldGroup, Field, Label, Description, ErrorMessage |
| Forms | Input | — | Text input — label, hint, error, left icon, right action slot, corner-hint slot |
| Forms | OtpInput | — | One-time password digit entry — sm / md / lg sizes, auto-advance, paste support |
| Forms | RadioGroup | — | Labelled radio button group — default and card variants |
| Forms | RatingGroup | — | Star rating — controlled / uncontrolled, half-steps, custom icons, RTL |
| Forms | SearchSet | — | Search field with tag chips and a collapsible filter panel |
| Forms | Select | Listbox |
Single-select dropdown with animated panel |
| Forms | Textarea | — | Multi-line input with label, hint, error, character count |
| Forms | TimePicker | Popover |
Scrollable HH / MM column time picker — Clear / Now shortcuts |
| Forms | Toggle | Switch |
Accessible on/off switch with optional label and description |
| Headings | PageHeading | — | Page-level heading — title, back link, meta slot, tabs, action buttons, optional sticky |
| Headings | SectionHeader | Menu |
Section heading with optional overflow action menu |
| Headings | SectionHeading | — | Lightweight sub-section label with optional tab strip |
| Layout | Card | — | Content panel — default, muted, outlined, and flush variants |
| Layout | ContainerList | — | Stacked list of card-style containers with action buttons |
| Layout | Divider | — | Horizontal rule with optional centred or left-aligned label |
| Layout | ListCard | — | Card with a built-in divided list slot |
| Layout | MediaObject | — | Icon / image + text side-by-side layout primitive — left / right, gap variants |
| Layout | PageContainer | — | Max-width centred page wrapper — xs / sm / md / lg / xl / full widths |
| Lists | Carousel | — | Scrollable card carousel with dot indicators and auto-play |
| Lists | DataTable | — | Sortable, clickable table with typed column headers and pagination |
| Lists | GridList | Menu |
Card-grid list with per-item overflow action menu |
| Lists | StackedList | — | Divided list — bordered card or flat; custom renderItem slot |
| Data Display | Calendar | — | Monthly event calendar or compact mini picker with range selection and month/year quick-picker |
| Data Display | DescriptionList | — | Label / value definition list — stacked and side-by-side layouts |
| Data Display | Stats | — | KPI stat cards — cards, simple, and bordered variants; trend indicators |
| Navigation | BottomNav | — | Mobile bottom tab bar — fixed or inline, icon + label, badge support |
| Navigation | Breadcrumbs | — | Hierarchical path trail with configurable separators |
| Navigation | Combobox | Combobox |
Searchable select with real-time filtering and custom option rendering |
| Navigation | FabMenu | — | Floating action button with expandable radial / stacked item menu |
| Navigation | FlyoutMenu | Popover |
Mega-menu flyout — simple list, featured, and full-width variants |
| Navigation | Navbar | Menu |
Top navigation bar — logo, nav links, search, actions, user dropdown |
| Navigation | Pagination | — | Page navigation with prev / next, page numbers, and jump-to input |
| Navigation | ProgressBar | — | Linear and circular progress bars — labelled, striped, and animated variants |
| Navigation | SidebarNav | Menu, Disclosure |
Full-height sidebar — logo, grouped nav, collapsible sections, user profile |
| Navigation | StepsBar | — | Multi-step progress indicator — horizontal and vertical orientations |
| Navigation | Tabs | TabGroup |
Horizontal tab strip — underline and pill variants |
| Navigation | VerticalNav | — | Standalone vertical nav list — size, spacing, radius, shadow, border, active-indicator props |
| Overlays | Dialog | Dialog |
Modal with focus trap, backdrop, animated entry · sm / md / lg / xl sizes |
| Overlays | Drawer | Dialog |
Slide-in panel — left / right / bottom placements, configurable sizes |
| Overlays | Notifications | — | Toast notification stack — info, success, warning, error; auto-dismiss; position variants |
| Overlays | Tooltip | — | Hover/focus label — configurable placement, radius, and max-width (Floating UI) |
| Feedback | Alert | — | Inline alert — info, success, warning, error; dismissible; with-actions variant |
| Feedback | EmptyState | — | Zero-state placeholder with icon, heading, body, and primary / secondary actions |
| Feedback | ErrorPage | — | Full-page error states — 404, 500, 403, 503; icon or large code display |
| Marketing | Banner | — | Announcement banner — dark, primary, and light variants; dismissible |
| Messaging | Chat | — | iMessage-style chat thread — bubbles, timestamps, read receipts, reactions, image lightbox |
| Example Pages | DetailScreen | — | Detail page with stacked (Navbar) or sidebar (SidebarNav) chrome, PageHeading, DescriptionList, attachments, activity feed |
| Example Pages | HomeScreen | — | Dashboard with sidebar (deployments + activity) or stacked (cashflow + transactions) layout |
| Example Pages | Login | — | Standard email + password, passwordless OTP, and email + 2FA authenticator login flows |
| Example Pages | Register | — | Single-step and multi-step (with OTP verification) registration flows |
PRs and issues are welcome. The CI gate runs TypeScript, build, and interaction tests in both light and dark mode on every push and PR.
pnpm storybook # develop with hot reload
pnpm test-storybook:light # run tests locally (light)
pnpm test-storybook:dark # run tests locally (dark)
pnpm typecheck # type-checkLogo —
assets/gp-logo.*is proprietary and excluded from the MIT licence. All other files are MIT.
pnpm build # compile to dist/ (ESM + CJS + .d.ts)
pnpm dev # watch mode
pnpm typecheck # tsc --noEmit
pnpm clean # rm -rf dist/# Bump version and publish
npm version patch # or minor / major
pnpm build
pnpm publish --access publicOr tag a release to trigger the GitHub Actions publish workflow:
git tag v0.2.0 && git push --tags