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 — Phosphor Icons
Section titled “Icon Library — Phosphor Icons”Package: @phosphor-icons/react v2
All core product apps and packages/ui use Phosphor exclusively. New shared UI or product-app code
must not introduce lucide-react.
Weight System
Section titled “Weight System”Phosphor provides 6 weights that establish visual hierarchy without relying on color:
| Weight | Use case |
|---|---|
thin | Empty states, large decorative page header icons |
light | Secondary actions, supplemental labels |
regular | Nav, buttons, form controls — default |
bold | Dense UI at 12px where regular loses legibility |
fill | Active / selected states |
duotone | Premium highlight moments |
import { MagnifyingGlass, CaretDown, CircleNotch, ShieldCheck } from '@phosphor-icons/react';
// Default (regular weight) — matches prior Lucide behavior<MagnifyingGlass className="h-4 w-4" />
// Explicit weight for context<CircleNotch className="animate-spin" weight="regular" /><ShieldCheck weight="fill" className="text-emerald-500" /> // active/selectedKey Lucide → Phosphor Name Changes
Section titled “Key Lucide → Phosphor Name Changes”| Lucide | Phosphor |
|---|---|
ChevronLeft/Right/Down/Up | CaretLeft/Right/Down/Up |
ChevronsUpDown | CaretUpDown |
Search | MagnifyingGlass |
Loader2, LoaderIcon | CircleNotch |
AlertCircle | WarningCircle |
AlertTriangle | Warning |
MoreHorizontal | DotsThree |
MoreVertical | DotsThreeVertical |
Settings | Gear |
DollarSign | CurrencyDollar |
TrendingUp / TrendingDown | TrendUp / TrendDown |
Mail | Envelope |
Save | FloppyDisk |
Download | DownloadSimple |
ExternalLink | ArrowSquareOut |
Maximize2 | CornersOut |
Edit, Edit2 | PencilSimple |
Layers | Stack |
BarChart2, BarChart3 | ChartBar |
MessageCircle | ChatCircle |
MessageSquare | Chat |
Sparkles | Sparkle |
RefreshCw | ArrowsClockwise |
RefreshCcw | ArrowCounterClockwise |
Scale | Scales |
Building2 | Buildings |
Landmark | Bank |
ShieldAlert | ShieldWarning |
ShieldX | ShieldSlash |
BadgeCheck | SealCheck |
Award | Medal |
Zap | Lightning |
Send | PaperPlaneRight |
Trash2 | Trash |
FileSpreadsheet | FileXls |
CheckCircle2 | CheckCircle |
Home | House |
HelpCircle | Question |
XIcon | X |
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