Theming

Tokens, the light and dark palettes, and theming a navigator from them.

theme.css is the one file that is yours. Everything else init puts in styles/ is plumbing — the engines, the size scale, the barrel — and none of it needs touching to retheme an app. Replace theme.css whole and every component repaints.

Three ways to fill it:

  1. Build one. Theme composes seven axes and shows the file. Copy the theme.css tab over yours. Nothing to run.
  2. Bring a web app's theme. Paste a shadcn or tweakcn globals.css over theme.css and run delacour theme, which rewrites the file in place.
  3. Edit it. The palette is shadcn's, name for name, so the names are the ones you already know.

The palette is shadcn's, in shadcn's shape. A theme built for a web app — by hand, by shadcn init, or in a generator like tweakcn — is the same set of variable names this library paints from, so moving one across is a copy, not a translation.

Two layers, in theme.css. The raw semantic names are declared once per theme under @variant light / @variant dark, and an @theme inline block maps each onto the --color-* Tailwind mints utilities from:

@layer theme {
  :root {
    @variant light { --primary: oklch(0.205 0 0); }
    @variant dark  { --primary: oklch(0.922 0 0); }
  }
}

@theme inline {
  --color-primary: var(--primary);
}

Components reference the semantic name — bg-primary, text-muted-foreground — and never a raw palette colour or a dark: prefix. The variable swap handles the theme.

Bring your web theme

Paste the file over theme.css, then:

bunx delacour@latest theme

Or point the command at the source and it writes theme.css for you:

bunx delacour@latest theme ./src/app/globals.css
bunx delacour@latest theme https://tweakcn.com/r/themes/<id>

It also reads stdin, which is the shortest path from a generator's Copy button:

pbpaste | bunx delacour@latest theme -

The one thing that cannot survive a straight paste is the wrapper. Uniwind reads a theme only from @variant light / @variant dark; a literal .dark { … } is registered as a utility class named dark and contributes nothing — no error, no warning, just a dark theme that never arrives. The command rewrites those two lines, fills in the tokens shadcn has no name for, and reports anything React Native cannot use. delacour doctor fails on a theme.css still in shadcn's shape, so a forgotten run is caught before a build.

Font stacks are the one value that does not carry. React Native's fontFamily takes a single family name with no fallback list, so "Inter, ui-sans-serif, system-ui" resolves to nothing and falls back to the system sans. delacour theme keeps the first family and tells you what it dropped.

The palette

Every token shadcn defines, under the name shadcn gives it — plus elevated, tertiary, success, warning, info and overlay, which it has no name for and which every component here paints with. Those six are derived from tokens a shadcn theme does define, so a palette you bring across drags them along instead of leaving them on this package's defaults.

Tokens is the full reference: what each one means, what it resolves to in both themes, and how each derivation is built.

Overriding a token

Redeclare it in your own CSS, after the library's import. Raw names, not --color-*.

global.css
@import "@delacour/react-native-ui/styles";

@layer theme {
  :root {
    @variant light {
      --primary: oklch(0.488 0.243 264.376);
      --primary-foreground: oklch(0.985 0 0);
    }
    @variant dark {
      --primary: oklch(0.707 0.165 254.624);
      --primary-foreground: oklch(0.205 0 0);
    }
  }
}

Declare a token in both variants. Uniwind refuses to build a theme that declares a variable the other one does not — "All themes must have the same variables".

Corners

Every generic rounded-* step is derived from a single --radius, so setting it retunes the whole library at once.

@theme { --radius: 0.25rem; }

rounded-lg is --radius exactly; the steps either side are multiples of it, which is what keeps a square-cornered theme square at every step. A button is the exception — its corner is half its own height, so it stays a capsule however --radius is set. See Sizing.

Reading a colour from JavaScript

A class cannot express a literal (#EC4899) and cannot reach an SVG paint prop like a gradient's stopColor. For a prop that needs a colour value, use useThemeColor.

import { useThemeColor } from "@delacour/react-native-ui/hooks/use-theme-color";

const destructive = useThemeColor("destructive");

Size is a class, colour is a token

The asymmetry is deliberate. Tailwind's spacing scale genuinely owns 14/16/18/20/22/24/32, so a class is the better name for those. A colour class could not express a literal or reach an SVG paint prop, and useThemeColor already covers both — so a colour-class path would leave two colour systems in the package rather than replacing one.

Theming a navigator

A navigator paints its own chrome from its own theme, and nothing connects that to these tokens. expo-router mounts its NavigationContainer with no theme, so React Navigation's light default stands however dark your app is. On iOS the native stack hands colors.background to the UINavigationController's view — the slab visible between cards during a push, and at their rounded corners. That layer has no escape hatch: a contentStyle in screenOptions cannot reach it. Only a theme fixes it.

app/_layout.tsx
import { NavigationTheme } from "@delacour/react-native-ui/expo/navigation-theme";

<DelacourProvider>
  <NavigationTheme>
    <Stack />
  </NavigationTheme>
</DelacourProvider>;

On a different navigator, use the hook directly. It returns plain values, not a Theme — the library imports nothing from a navigation package.

import { useNavigationTheme } from "@delacour/react-native-ui/hooks/use-navigation-theme";

const { dark, ...colors } = useNavigationTheme();

<NavigationContainer theme={{ ...DefaultTheme, dark, colors: { ...DefaultTheme.colors, ...colors } }}>

Spread them over the framework's own base theme, which also supplies fonts — a platform-specific shape that would drift if restated here.

dark comes from Uniwind's active theme, never React Native's useColorScheme. An app that lets the user force light or dark against the system setting would otherwise theme its navigator the wrong way round.

Generating one

If you would rather build a palette than paste one, Theme composes seven orthogonal axes — style, radius, base colour, accent, chart hues and two typefaces — and shows the theme.css they add up to, in exactly the shape init ships. Copy it over yours; there is nothing to run. A second tab holds shadcn's globals.css for a web app that shares the theme. Each option is a link, so the whole theme is the code in the URL: share it, bookmark it, or open it on a phone.

The playground app has the same axes on its own Theme screen, where they repaint a real app as you go rather than a specimen. Its footer opens that page with the configuration already in the URL, which is the way a theme built on a device becomes a file.

On this page