Getting started
Theming
One accent, one radius and a light and dark palette drive every component. Each is a CSS variable switched by an attribute or class on <html>.
On this page
Three switches
Components never hard-code a color or a corner. They use semantic tokens like bg-accent, text-muted and rounded-ui, which read CSS variables. Changing a theme means changing variables, so the whole interface follows at once and nothing needs to re-render.
- Accent:
data-accenton<html>, 6 presets. - Radius:
data-radiuson<html>, 5 presets. - Mode: the
darkclass on<html>, light by default.
Accent
Mode
Radius
<html> Current theme: Graphite accent, Medium radius, light mode.
Accents
Graphite is the default: near-black in light mode and near-white in dark mode, for a quiet, editorial look. The other five are colored. Each preset defines the same five values for both modes, tuned so text on the accent and accent-colored text both pass contrast.
[data-accent="ocean"] { --pl-accent: #0b6bcb; --pl-accent-hover: #095aac; --pl-accent-fg: #ffffff; --pl-accent-soft: rgb(11 107 203 / 0.09); --pl-ring: rgb(11 107 203 / 0.4);}.dark[data-accent="ocean"] { --pl-accent: #5aa9ff; --pl-accent-hover: #7cbbff; --pl-accent-fg: #04182e; --pl-accent-soft: rgb(90 169 255 / 0.14); --pl-ring: rgb(90 169 255 / 0.45);}Custom accent
Add a preset with the same five variables, light first and dark second. Pick the dark value one or two steps lighter than the light one, and choose --pl-accent-fg so text on the accent reads at 4.5:1 or better. Set data-accent="teal" on <html> to use it.
/* app/globals.css: a "teal" preset next to the built-in ones */[data-accent="teal"] { --pl-accent: #0f766e; --pl-accent-hover: #0b5f58; --pl-accent-fg: #ffffff; --pl-accent-soft: rgb(15 118 110 / 0.09); --pl-ring: rgb(15 118 110 / 0.4);}.dark[data-accent="teal"] { --pl-accent: #3dd6c6; --pl-accent-hover: #63e0d3; --pl-accent-fg: #032623; --pl-accent-soft: rgb(61 214 198 / 0.14); --pl-ring: rgb(61 214 198 / 0.45);}Or change the defaults: the values in :root and .dark are what you get without any attribute. The theme customizer writes this CSS for you.
Radius
One variable, --pl-radius, sets the scale. Components pick a step by role: small things like menu items use rounded-ui-sm, controls use rounded-ui, cards use rounded-ui-lg and dialogs use rounded-ui-xl. Pills, avatars and switches stay rounded-full in every preset.
:root { --pl-radius: 0.625rem; } /* medium, the default */[data-radius="none"] { --pl-radius: 0rem; }[data-radius="small"] { --pl-radius: 0.375rem; }[data-radius="large"] { --pl-radius: 0.875rem; }[data-radius="full"] { --pl-radius: 1.25rem; } @theme inline { --radius-ui-sm: calc(var(--pl-radius) * 0.6); /* rounded-ui-sm: menu items, chips */ --radius-ui: var(--pl-radius); /* rounded-ui: buttons, inputs */ --radius-ui-lg: calc(var(--pl-radius) * 1.4); /* rounded-ui-lg: cards, popovers */ --radius-ui-xl: calc(var(--pl-radius) * 2); /* rounded-ui-xl: dialogs, sheets */}Dark mode
Plinth defaults to light. Dark mode is the dark class on <html>, which swaps the neutral palette and the accent values. Because components only use tokens, you never write dark: color overrides. Code blocks stay dark in both modes.
@custom-variant dark (&:where(.dark, .dark *)); :root { --pl-canvas: #fbfbfa; --pl-surface: #ffffff; --pl-fg: #18181b; /* … */ }.dark { --pl-canvas: #0c0c0e; --pl-surface: #141417; --pl-fg: #f4f4f5; /* … */ }"System" is not a third palette: it picks light or dark from prefers-color-scheme when the page loads and when the setting changes.
How it works
The choice is saved in localStorage under three keys: theme (light, dark or system), accent and radius. Defaults are expressed as no attribute: graphite has no data-accent and medium has no data-radius.
The boot script
A tiny inline script in <head> reads the three keys and sets the class and attributes before the first paint, so there is no flash of the wrong theme. Keep suppressHydrationWarning on <html> because the script changes it before React hydrates.
(function () { try { var s = localStorage, e = document.documentElement; var t = s.getItem("theme"); // "light" | "dark" | "system" var dark = t === "dark" || (t === "system" && matchMedia("(prefers-color-scheme: dark)").matches); e.classList.toggle("dark", dark); var a = s.getItem("accent"); if (a) e.setAttribute("data-accent", a); var r = s.getItem("radius"); if (r) e.setAttribute("data-radius", r); } catch (x) {}})();Switching in code
Three small functions cover every control: a toggle, a select, a settings page.
"use client"; export function setAccent(id: string) { const el = document.documentElement; if (id === "graphite") el.removeAttribute("data-accent"); else el.setAttribute("data-accent", id); localStorage.setItem("accent", id);} export function setRadius(id: "none" | "small" | "medium" | "large" | "full") { const el = document.documentElement; if (id === "medium") el.removeAttribute("data-radius"); else el.setAttribute("data-radius", id); localStorage.setItem("radius", id);} export function setMode(mode: "light" | "dark" | "system") { const dark = mode === "dark" || (mode === "system" && matchMedia("(prefers-color-scheme: dark)").matches); document.documentElement.classList.toggle("dark", dark); localStorage.setItem("theme", mode);}Shipping a single, fixed theme? Skip the script and set it in the markup:
<!-- Ship one theme only: set the attributes on <html> and drop the boot script --><html lang="en" data-accent="forest" data-radius="large" class="dark">Theme tokens
These are the variables a theme changes. The full list, with values for both modes, is on the design tokens page.
- --pl-accentbg-accent, text-accent, border-accentPrimary actions, selection, links
- --pl-accent-hoverhover:bg-accent-hoverHover state of accent fills
- --pl-accent-fgtext-accent-fgText and icons on accent fills
- --pl-accent-softbg-accent-softSelected rows, soft badges, highlights
- --pl-ringring-ring, outlineFocus rings
- --pl-radiusrounded-ui-sm … rounded-ui-xlEvery corner on component surfaces
- --pl-canvas … --pl-sunkenbg-canvas, bg-surface, bg-raised, bg-sunkenSurfaces, switch with .dark
- --pl-fg, --pl-muted, --pl-subtletext-fg, text-muted, text-subtleText, switch with .dark