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

Phosphor provides 6 weights that establish visual hierarchy without relying on color:

WeightUse case
thinEmpty states, large decorative page header icons
lightSecondary actions, supplemental labels
regularNav, buttons, form controls — default
boldDense UI at 12px where regular loses legibility
fillActive / selected states
duotonePremium 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/selected
LucidePhosphor
ChevronLeft/Right/Down/UpCaretLeft/Right/Down/Up
ChevronsUpDownCaretUpDown
SearchMagnifyingGlass
Loader2, LoaderIconCircleNotch
AlertCircleWarningCircle
AlertTriangleWarning
MoreHorizontalDotsThree
MoreVerticalDotsThreeVertical
SettingsGear
DollarSignCurrencyDollar
TrendingUp / TrendingDownTrendUp / TrendDown
MailEnvelope
SaveFloppyDisk
DownloadDownloadSimple
ExternalLinkArrowSquareOut
Maximize2CornersOut
Edit, Edit2PencilSimple
LayersStack
BarChart2, BarChart3ChartBar
MessageCircleChatCircle
MessageSquareChat
SparklesSparkle
RefreshCwArrowsClockwise
RefreshCcwArrowCounterClockwise
ScaleScales
Building2Buildings
LandmarkBank
ShieldAlertShieldWarning
ShieldXShieldSlash
BadgeCheckSealCheck
AwardMedal
ZapLightning
SendPaperPlaneRight
Trash2Trash
FileSpreadsheetFileXls
CheckCircle2CheckCircle
HomeHouse
HelpCircleQuestion
XIconX

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