Skip to content

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-accent on <html>, 6 presets.
  • Radius: data-radius on <html>, 5 presets.
  • Mode: the dark class on <html>, light by default.

Accent

Mode

Radius

Draft Auto-save

<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.

Graphite#18181b · #f4f4f5
Ocean#0b6bcb · #5aa9ff
Forest#17803f · #4ccf7f
Ember#e0531f · #ff8452
Violet#6d4aff · #a48dff
Rose#d62f6a · #ff6b9e
app/globals.css
[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
/* 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.

None0rem
Small0.375rem
Medium0.625rem
Large0.875rem
Full1.25rem
app/globals.css
: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.

app/globals.css
@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.

Inline in app/layout.tsx
(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.

lib/theme.ts
"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:

app/layout.tsx
<!-- 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

Suggested