Design System
OpenInsure uses a unified premium design system across all five Next.js portals and the shared packages/ui component library. This document is the canonical reference for contributors.
The canonical implementation surfaces are:
packages/ui/src/*for shared primitives, tokens, and helpersapps/ladlefor visual reference and regression review- this document for contributor-facing rules
If another document conflicts with this one, this document and the shared package win.
Font Stack
Section titled “Font Stack”Three variable fonts, loaded via next/font/google in each portal’s layout.tsx:
| Role | Font | CSS variable | Axes |
|---|---|---|---|
| Heading / display | Bricolage Grotesque | --font-heading | wght 200–800, wdth 75–100 |
| Body / UI | Geist | --font-sans | wght 100–900 |
| Data / code | Geist Mono | --font-mono | wght 100–900 |
Why These Fonts
Section titled “Why These Fonts”Bricolage Grotesque — Architectural character at bold weights, semi-condensed forms that read as precision. Extremely rare in insurtech; provides genuine differentiation from the Inter/Outfit saturation in 2024–2025 B2B SaaS.
Geist — Already partially deployed in the admin portal before the upgrade. Cleaner than Inter at 14px dense table/form layouts; narrower apertures make the alphabet purposeful at small sizes.
Geist Mono — Perfect companion to Geist. Provides the slashed zero that distinguishes 0 from O in policy numbers, claim IDs, and premium values. Tabular figures align decimal columns precisely.
Loading Pattern (Next.js portals)
Section titled “Loading Pattern (Next.js portals)”// apps/<portal>/app/layout.tsximport { Bricolage_Grotesque, Geist, Geist_Mono } from 'next/font/google';
const bricolage = Bricolage_Grotesque({ subsets: ['latin'], variable: '--font-heading', display: 'optional',});const geist = Geist({ subsets: ['latin'], variable: '--font-sans', display: 'optional',});const geistMono = Geist_Mono({ subsets: ['latin'], variable: '--font-mono', display: 'optional',});
export default function RootLayout({ children }) { return ( <html className={`${bricolage.variable} ${geist.variable} ${geistMono.variable}`}> <body>{children}</body> </html> );}next/font/google self-hosts the font files at build time — no cross-origin DNS hit, no CLS, correct font-display: optional behavior.
Ladle Exception
Section titled “Ladle Exception”Ladle uses Vite (no next/font). Fonts are loaded from Google Fonts CDN at the top of apps/ladle/.ladle/ladle.css:
@import url('https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wdth,wght@12..60,75..100,200..800&family=Geist:wght@100..900&family=Geist+Mono:wght@100..900&display=swap');Compatibility Shim
Section titled “Compatibility Shim”@openinsure/ui/fonts/fintech.css remains exportable so older app imports do not break, but it is
now only a compatibility shim. It resolves to the same Bricolage/Geist/Geist Mono stack rather than
defining a separate font pairing policy.
Icon Library - Lucide Icons
Section titled “Icon Library - Lucide Icons”Package: lucide-react
All web product apps and packages/ui use Lucide directly. New shared UI or product-app code must
not introduce @phosphor-icons/react or the deprecated @openinsure/ui/icons barrel.
import { ChevronDown, LoaderCircle, Search, ShieldCheck } from 'lucide-react';
<Search className="size-4" /><ChevronDown className="size-4" aria-hidden="true" /><LoaderCircle className="size-4 animate-spin" aria-hidden="true" /><ShieldCheck className="size-4 text-success" aria-hidden="true" />Icon Rules
Section titled “Icon Rules”Use Lucide icon buttons with accessible labels or tooltips for familiar actions such as save,
edit, delete, copy, download, upload, close, expand, refresh, search, filter, and sort. Import from
lucide-react in the file that renders the icon so review tooling can detect drift.
Design Tokens
Section titled “Design Tokens”All tokens live in packages/ui/src/globals.css and are available as Tailwind utilities.
Shadow System
Section titled “Shadow System”oklch-precise shadows that are dark-mode-aware via CSS variable references:
/* Light */--shadow-card: 0 1px 2px oklch(0 0 0 / 0.04), 0 0 0 1px oklch(0 0 0 / 0.04);--shadow-brand: 0 4px 14px oklch(0.21 0.006 285.885 / 0.12);--shadow-focus: 0 0 0 3px oklch(0.21 0.006 285.885 / 0.15);
/* Dark overrides */.dark { --shadow-card: 0 1px 2px oklch(1 0 0 / 0.04), 0 0 0 1px oklch(1 0 0 / 0.04);}Use as Tailwind utilities: shadow-card, shadow-brand, shadow-focus, or inline: shadow-[var(--shadow-brand)].
Radius Scale
Section titled “Radius Scale”Absolute values that eliminate the non-integer px rounding from the old calc(var(--radius) - 4px) chain:
| Token | Value | px |
|---|---|---|
--radius-xs | 0.25rem | 4px |
--radius-sm | 0.375rem | 6px |
--radius-md | 0.5rem | 8px |
--radius-lg | 0.625rem | 10px (base) |
--radius-xl | 0.75rem | 12px |
--radius-2xl | 1rem | 16px |
--radius-3xl | 1.5rem | 24px |
--radius-4xl | 2rem | 32px |
Domain Typography Utilities
Section titled “Domain Typography Utilities”Defined in @layer utilities in packages/ui/src/globals.css. Apply directly in JSX:
// Premium value in a KPI card<div className="stat-value text-3xl">{value}</div>
// KPI label<p className="metric-label">Written Premium</p>
// Data table column header<th className="label-mono">Policy Number</th>
// Currency display<span className="currency-display text-xl">$1,847,293.45</span>
// Hero / marketing heading<h2 className="heading-display">The Operating System for Modern Insurance</h2>| Class | Font | Size | Transform | Numeric |
|---|---|---|---|---|
.stat-value | Geist Mono | inherited | — | tabular-nums lining-nums slashed-zero |
.metric-label | system (Geist) | 11px | uppercase + 0.06em | tabular-nums |
.label-mono | Geist Mono | 11px | uppercase + 0.08em | tabular-nums |
.currency-display | Geist Mono | inherited | — | tabular-nums lining-nums slashed-zero |
.heading-display | Bricolage Grotesque | inherited | — | — |
Quality check: Render $1,234.56 and $987.00 side-by-side using .stat-value. The decimal points must vertically align. This is the single highest-signal test for the stat-value utility.
Status Badge Colors
Section titled “Status Badge Colors”StatusBadge in packages/ui uses CSS custom property classes — not Tailwind named color utilities. This keeps light/dark theming correct without per-mode class overrides:
| Class | Use cases |
|---|---|
status-neutral | received, expired, closed, inactive |
status-success | bound, active, approved, completed, authorized |
status-danger | declined, cancelled, denied, rejected, revoked, siu, out_of_service |
status-warning | incomplete, open |
status-process | extracting, scheduled |
status-purple | rated |
status-sky | quoted |
status-orange | referred |
status-indigo | in_progress |
status-yellow | pending |
Heading Scale (@layer base)
Section titled “Heading Scale (@layer base)”Applied automatically to bare h1–h4 elements:
| Element | Size | Weight | Tracking |
|---|---|---|---|
h1 | 1.75rem | 700 | -0.04em |
h2 | 1.375rem | 600 | -0.03em |
h3 | 1.125rem | 600 | -0.02em |
h4 | 1rem | 600 | -0.015em |
All use Bricolage Grotesque via var(--font-heading).
Verification with Ladle
Section titled “Verification with Ladle”# Start Ladle dev serverpnpm --filter @openinsure/ladle dev# → http://localhost:61000
# Key stories to verify# Typography → HeadingScale — Bricolage Grotesque rendering# Typography → DomainUtilities — decimal alignment test# Iconography → WeightSystem — 6-weight hierarchy# Iconography → DomainIcons — insurance-specific icons# KpiCard → Default — stat-value + metric-label + shadow-brand