Theming

Class-based dark mode with a no-flash inline script, system preference support, and a ready-made toggle.

Try it

Current theme

Preference: system · Resolved: light

Setup

Wrap your app once. ThemeScript applies the stored theme before first paint so there is no flash of the wrong mode.

// app/layout.tsx
import { ThemeProvider, ThemeScript } from '@tra/ui';
export default function RootLayout({ children }) {
return (
<html lang="en" suppressHydrationWarning>
<body>
<ThemeScript />
<ThemeProvider>{children}</ThemeProvider>
</body>
</html>
);
}

Reading and setting the theme

'use client';
import { useTheme, ThemeToggle } from '@tra/ui';
function Settings() {
const { theme, resolvedTheme, setTheme } = useTheme();
return (
<>
<ThemeToggle /> {/* or roll your own: */}
<button onClick={() => setTheme('dark')}>Dark</button>
<button onClick={() => setTheme('system')}>Match system</button>
</>
);
}

How it works

  • Dark mode is the .dark class on <html> — every semantic token swaps automatically.
  • The preference persists in localStorage under tra-theme.
  • system tracks the OS and updates live when the OS preference changes.
  • Components built with semantic tokens (bg-canvas, text-ink, border-line) never need dark: overrides.