A point of sale is used for eight hours straight, often in a bright shop and sometimes in a dim one. So Tilly POS needed a calm interface, a real dark mode, and — because I'm one developer — a way to get both without writing every style twice.

The answer is boring and it works: design tokens as CSS custom properties, and components that only ever use tokens.

Tokens: names for decisions, not colours

:root, [data-theme="light"] { --bg: #f8fafc; --surface: #ffffff; --surface-muted: #f1f5f9; --border: #e2e8f0; --ink: #0f172a; --ink-muted: #475569; --primary: #4f46e5; --primary-hover: #4338ca; } [data-theme="dark"] { --bg: #020617; --surface: #0f172a; --surface-muted: #1e293b; --border: #1e293b; --ink: #f1f5f9; --ink-muted: #94a3b8; --primary: #818cf8; --primary-hover: #a5b4fc; } 

The names describe what the colour is for — background, surface, ink, primary — never what it looks like. --ink is near-black in light mode and near-white in dark mode, and no component needs to know.

Components: only tokens, no raw colours

.pos-btn--primary { background: var(--primary); color: var(--on-primary); } .pos-btn--primary:hover { background: var(--primary-hover); } .pos-btn--secondary { background: var(--surface); color: var(--ink); border-color: var(--border-strong); } .pos-btn--secondary:hover { background: var(--surface-muted); } 

Switch the theme with one attribute:

<html data-theme="dark"> 

Every component follows, because none of them contain a colour of their own. Adding a third theme (high contrast, a client's brand) is one more block of variables.

Rules that kept it calm

These came from the design guidelines I wrote before building any screen:

  1. One primary action per view. Exactly one primary button; everything else is secondary or ghost.
  2. Status is never colour alone. "Paid", "Pending", "Refunded" always have a word and an icon, so they work for colour-blind users and in dark mode.
  3. Borders, not shadows. Structure comes from hairline borders on surfaces; shadows only lift things that float, like dialogs and toasts.
  4. Numbers line up. Every amount uses font-variant-numeric: tabular-nums, so columns of money align.
  5. Plain CSS classes, framework-neutral. The components are pos- classes, so the same markup works in Angular, React, Vue, Blazor or a static page. In Angular they sit in Tailwind v4's components layer.

The result

The same dashboard and checkout render in light and dark from one stylesheet, and adding a screen means composing existing classes instead of inventing colours.

I've packaged the whole system — 19 components, the dashboard and checkout screens, light and dark tokens (CSS and JSON), Lucide icons and an Angular + Tailwind v4 setup guide — as Tilly UI.

How do you handle themes — CSS variables, Tailwind's dark: variant, or something else?