React UI Design System Foundations
Every Liro component reads its colours from one theme file, written in the shadcn/ui token format. Keep your own globals.css and the components follow it; or import the Liro theme and get the look below, in light and dark.
@import "tailwindcss"; @import "./liro.css";
The components use the Geist font. The theme file names it but does not load it, so load Geist in your app and point --font-sans at it. If you skip this, the components fall back to the system font and still work. To use your own font instead, put it in --font-sans the same way.
// app/layout.tsx import { Geist } from "next/font/google"; const geist = Geist({ subsets: ["latin"], variable: "--font-geist" }); // <html lang="en" className={geist.variable}> /* app/globals.css, after the liro.css import */ @theme { --font-sans: var(--font-geist), ui-sans-serif, system-ui, sans-serif; }
Surfaces
Tailwind’s neutral scale. Resting surfaces are flat with a hairline; only things that float get a shadow.
--backgroundPage ground
--cardResting surfaces
--mutedRows, tracks, quiet fills
--accentHover
--primaryThe one action
Ink and edges
Borders are translucent ink rather than a grey, so the same hairline reads correctly on a card, a muted row and a tinted status panel.
--foregroundText you read first
--muted-foregroundMetadata, captions
--borderHairlines, translucent ink
--inputField and control edges
--ringFocus
Status
Four meanings and no more. shadcn defines destructive; Liro adds success, warning and info in the same format. Warning has a brighter --warning-fill for meters and icons, because legible amber text and a visible amber bar are not the same colour.
--destructiveDanger, failure, the irreversible. #E5484D; #C0272C as light-theme text
--successPaid, approved, resolved
--warningHeld, pending, not settled yet
--infoNeutral emphasis, selection
The Liro look
Panels and lists come in two looks. Modern rests white cards on a gray tray; flat is one card alone. Loose controls (buttons, fields, chat bubbles) take no tray in either look. The tray is its own token, --tray, so a brand can tint it. Corners are continuous (iOS-style squircles) and step down with depth: 26px for the tray, 20px for the card in it.
light
Modern
Card in the tray · #F0F0F0 tray
Status strip in the tray’s margin
Flat
One card, hairline and contact shadow
dark
Modern
Card in the tray · #0E0E10 tray
Status strip in the tray’s margin
Flat
One card, hairline and contact shadow
| Colour | Light | Dark |
|---|---|---|
| Tray (--tray) | #F0F0F0 | #0E0E10 |
| Red (--destructive) | #E5484D, #C0272C for text on white | #E5484D |
| Width steps | 360 · 440 · 640 · full width for dashboards | same |
Type
A product scale, not a reading one: small, tight, with half-pixel steps where a label and a caption need to differ. Available as text-title, text-meta and the rest.
figure27pxThe one number a component exists to showtitle15pxDialog and card titlesbody13.5pxRow titles, button labelssub13pxDescriptions under a titledense12.5pxTable cells, dense rowsmeta11.5pxTimestamps, sizes, countschip11pxChips and pillsmicro10.5pxEyebrows, the smallest legible labelRadius
Squircle corners where the browser supports them, the plain radius where it does not. Nested surfaces step down.
surface
18px
panel
14px
row
12px
control
10px
chip
8px
Elevation
Two shadows, for the two things that float: menus and popovers, then dialogs.
resting
Hairline, no shadow
float
Menus, popovers, tooltips
overlay
Dialogs and sheets
Motion
Springs for things that move, tweens for fades and colour. Everything respects reduced motion.
fast120msHover and press feedbackbase180msSmall elements entering and leavingslow260msPanels and dialogs
Layering
Popovers under dialogs under toasts, always, so a toast is never trapped behind the dialog that raised it.
popoverz-index 60dialogz-index 70toastz-index 80