Skip to content

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 helpers
  • apps/ladle for 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.

Three variable fonts, loaded via next/font/google in each portal’s layout.tsx:

RoleFontCSS variableAxes
Heading / displayBricolage Grotesque--font-headingwght 200–800, wdth 75–100
Body / UIGeist--font-sanswght 100–900
Data / codeGeist Mono--font-monowght 100–900

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.

// apps/<portal>/app/layout.tsx
import { 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 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');

@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.

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" />

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.

All tokens live in packages/ui/src/globals.css and are available as Tailwind utilities.

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)].

Absolute values that eliminate the non-integer px rounding from the old calc(var(--radius) - 4px) chain:

TokenValuepx
--radius-xs0.25rem4px
--radius-sm0.375rem6px
--radius-md0.5rem8px
--radius-lg0.625rem10px (base)
--radius-xl0.75rem12px
--radius-2xl1rem16px
--radius-3xl1.5rem24px
--radius-4xl2rem32px

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>
ClassFontSizeTransformNumeric
.stat-valueGeist Monoinheritedtabular-nums lining-nums slashed-zero
.metric-labelsystem (Geist)11pxuppercase + 0.06emtabular-nums
.label-monoGeist Mono11pxuppercase + 0.08emtabular-nums
.currency-displayGeist Monoinheritedtabular-nums lining-nums slashed-zero
.heading-displayBricolage Grotesqueinherited

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.

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:

ClassUse cases
status-neutralreceived, expired, closed, inactive
status-successbound, active, approved, completed, authorized
status-dangerdeclined, cancelled, denied, rejected, revoked, siu, out_of_service
status-warningincomplete, open
status-processextracting, scheduled
status-purplerated
status-skyquoted
status-orangereferred
status-indigoin_progress
status-yellowpending

Applied automatically to bare h1h4 elements:

ElementSizeWeightTracking
h11.75rem700-0.04em
h21.375rem600-0.03em
h31.125rem600-0.02em
h41rem600-0.015em

All use Bricolage Grotesque via var(--font-heading).

Terminal window
# Start Ladle dev server
pnpm --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