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.

app/globals.cssDownload liro.css
@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.

Next.js · app/layout.tsx and app/globals.css
// 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.

--background

Page ground

--card

Resting surfaces

--muted

Rows, tracks, quiet fills

--accent

Hover

--primary

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

--foreground

Text you read first

--muted-foreground

Metadata, captions

--border

Hairlines, translucent ink

--input

Field and control edges

--ring

Focus

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.

--destructive

Danger, failure, the irreversible. #E5484D; #C0272C as light-theme text

--success

Paid, approved, resolved

--warning

Held, pending, not settled yet

--info

Neutral 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

ColourLightDark
Tray (--tray)#F0F0F0#0E0E10
Red (--destructive)#E5484D, #C0272C for text on white#E5484D
Width steps360 · 440 · 640 · full width for dashboardssame

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 show
title15pxDialog and card titles
body13.5pxRow titles, button labels
sub13pxDescriptions under a title
dense12.5pxTable cells, dense rows
meta11.5pxTimestamps, sizes, counts
chip11pxChips and pills
micro10.5pxEyebrows, the smallest legible label

Radius

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 feedback
  • base180msSmall elements entering and leaving
  • slow260msPanels and dialogs

Layering

Popovers under dialogs under toasts, always, so a toast is never trapped behind the dialog that raised it.

  • popoverz-index 60
  • dialogz-index 70
  • toastz-index 80