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
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
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)
// 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 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
@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
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
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 |
Usage
import { MagnifyingGlass, CaretDown, CircleNotch, ShieldCheck } from '@phosphor-icons/react';
// Default (regular weight) — matches prior Lucide behavior
// Explicit weight for context
// active/selected
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 |
Phosphor has 9,000+ icons — 27× Lucide's 333. For domain concepts (Scales, Gavel, Certificate, HardHat) that Lucide lacks, check phosphoricons.com before creating custom SVGs.
Design Tokens
All tokens live in packages/ui/src/globals.css and are available as Tailwind utilities.
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
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
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
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)
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
# 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
In Ladle, fonts load from Google Fonts CDN (not next/font self-hosting). Check the Network tab
to confirm fonts.gstatic.com requests complete before running visual comparisons.