Skip to content

Contributing UI Guidelines

Vladimir Kromm edited this page Aug 27, 2026 · 5 revisions

UI & Design Guidelines

Target state for styling and components, decided in ADR-Styling-and-UI-Framework. This page covers the how — day-to-day conventions for building UI — the ADR covers the why.

Stack

Tailwind CSS for styling, shadcn/ui components (built on Radix UI) as the shared primitives layer.

Component layer

  • Anything interactive — dialogs, dropdowns, selects, tooltips, form controls — is built on a shadcn primitive, not hand-rolled. Radix (which shadcn wraps) solves focus management, keyboard navigation, and ARIA correctly; reimplementing that per-component is both wasted effort and an accessibility risk.
  • Add a new primitive with the shadcn CLI (npx shadcn add <name>) into src/components/ui/. It's copied into the repo, not installed as an opaque dependency — edit it freely once it's in.
  • Before adding a new primitive, check src/components/ui/ for one that already covers the need — including one that could reasonably be extended (a new prop, a variant) to cover it, not just an exact match. Prefer a small set of configurable primitives over a growing set of narrow, near-duplicate ones; a new need that's a variation on an existing primitive is a reason to extend it, not to add a sibling component next to it.
  • Purely presentational, non-interactive elements (a static card layout, a section header) don't need a shadcn primitive — compose them directly with Tailwind utilities.

Styling

  • Style with Tailwind utility classes in JSX. *.module.css is not used for new code.
  • Design tokens — color, spacing, radii, typography — are defined once in the Tailwind theme config, not as inline hex/px values scattered across components. If a value isn't in the theme yet and you need it more than once, add it to the theme rather than repeating a magic value.
  • Color palette stays deliberately small — a handful of named tokens, not a generated shade scale:
    • One neutral pair: background and foreground.
    • 1-2 brand colors.
    • 1-2 accent/highlight colors, for emphasis that isn't a status.
    • A fixed status set: success, warning, error, and info if the UI needs it. A new color need is first checked against this set before a new token is added; the set stays this small by design, not by oversight. Exact values are fixed when the theme is implemented (#117); this is the shape the token set must have.
  • Keep utility class lists readable: group related classes (layout, then spacing, then color/typography) rather than a random order, and extract to a component when a class list gets long enough to hurt readability rather than fighting it inline.

Theming

Light and dark themes are both supported, switchable at runtime — not light-only with dark bolted on later. Every color token above (neutral, brand, accent, status) is defined as a light/dark pair from the start; a component styled against the tokens works in both themes automatically. A component that hardcodes a color instead of using a token breaks this by construction.

Typography

Two font tokens, not a font per component:

  • A body font for all running text and UI copy — this is the default typeface.
  • A highlight font for emphasis: headings, callouts, or other spots that should stand apart from body text.

Both are defined once in the Tailwind theme and referenced by token, the same as color. Inter is already in use as the body font today (src/index.css) and is the natural default to carry forward; the highlight font is chosen when the theme is implemented (#117).

Composition

Feature components compose shadcn primitives and Tailwind utilities instead of raw HTML with a .module.css file. A modal like ShareModal becomes a shadcn Dialog with Tailwind-styled content, not a hand-rolled overlay div.

Migrating existing code

Code still on CSS Modules isn't required to migrate in a dedicated pass by default — when you touch a screen or component for other reasons, bring it in line with this guideline as part of that change rather than leaving it in a mixed state. Larger, dedicated migration passes are tracked as their own tickets when warranted.

Clone this wiki locally