Skip to content
txbrendan/ui

Theming

Theme more than colors: density, motion, type, shadows, and radius, per app or per component.

One theme

The theme install adds every token to your global CSS. That covers colors for light and dark, density, motion, type, shadows, and radius. Lint stops our shipped parts from using any color outside these tokens.

Dark mode turns on with the dark class on a parent, as in shadcn. You do not need a theme provider.

Tones and emphasis

Toned parts take a tone: neutral, brand, danger, success, and warning. Many also take an emphasis: solid, soft, outline, and ghost. Sizes are sm, md, and lg. Each component page lists the values it takes.

<Button tone="danger" emphasis="soft">Delete</Button>
<Badge tone="success">Paid</Badge>

Density

Density sets control heights, padding, and gaps. Set data-density to compact, comfortable, and spacious. Put it on html for the whole app, or on any element for one part of a page. With no attribute, you get comfortable.

<html data-density="compact">

<!-- Or for one part of a page -->
<section data-density="spacious">…</section>

Motion

Motion sets durations, easing, press feedback, and how popups close. Set data-motion to calm, crisp, playful, and off. With no attribute, you get crisp.

When someone asks their system for reduced motion, things still fade but never move, and loading pulses stay still. You do not need your own media query.

<html data-motion="calm">

Type, shadows, and radius

Text sizes, leading, and weights come from the --type-* tokens. Cards rest on shadow-card and popups float on shadow-float. Both read their own variables, with separate values for dark mode.

Website headlines use --font-headline, which is Geist Serif. Load it as --font-geist-serif, or headlines fall back to the system serif. App components stay in your sans.

// npm i geist
import { GeistSerif } from "geist/font/serif/variable";

// In app/layout.tsx
<html className={GeistSerif.variable}>

--radius sets corners everywhere. Many components also have their own knob, like --card-radius, so you can change one part without the rest.

:root {
  --radius: 0.5rem;        /* every corner */
  --card-radius: 0.75rem;  /* only cards */
}

For pill buttons and fields, run this after the theme. It makes buttons, toolbars, tabs, and fields fully round. Cards, menus, and text areas keep their corners. Or pick Pill in agent setup, and your theme ID carries it.

npx shadcn@latest add @txbrendan/pill

To go back, delete every line that names one of these knobs from your global CSS, in :root and in @theme inline.

--button-radius
--tool-button-radius
--toolbar-radius
--tabs-radius
--input-radius
--select-trigger-radius

Your brand color

The theme builder makes a light and dark theme from one brand color. Each one is checked against the same 81 contrast pairs as ours. Then you copy the CSS into your app.

The proof page says what the contrast check covers, and what it does not.