Skip to content
Protocore Design Systemv1.6.9

/// getting-started

Theming

Theming in PDS is declarative. Four data-attributes on any element — usually <html> — retune the token layer for everything inside it. No theme provider is required to change how the system looks.

The four axes

AttributeValuesControls
data-themedark · lightSurface, ink, and hairline palette. Dark is the default.
data-accentgreen · blue · red · amber · violetThe single reserved signal colour.
data-densitydefault · compactControl heights, body size, and section rhythm.
data-envdev · staging · prodEnvironment chrome colour (env badges and strips).

Attributes cascade and can be nested — set data-theme on <html> for the app, then override data-accent on a single panel to preview another accent locally.

Accents

Five curated accents ship with the system. Each is tuned for AA contrast in both themes — the light theme automatically darkens where a value would fail on white. The swatches below render the live --pds-accent value for each choice in the theme you are viewing.

  • green#3fcf8e
  • blue#2a6fdb
  • red#e8533f
  • amber#c9a227
  • violet#a855f7
Accent is a signal, not a palette
The accent marks the few things that matter — focus rings, the one live number, the selected item. It is never a button fill and never a status colour. Reach for the status tones (success, warning, danger, info, neutral) when you mean state. See Colors.

Environment chrome

data-env tints the environment chrome so a staging or production surface is unmistakable at a glance. It drives components like the env badge and env strip through the single --pds-color-env token.

  • dev
  • staging
  • prod

Density

data-density="compact" tightens control heights, shrinks the body size, and reduces vertical rhythm for dense console screens — without changing any component’s markup. Set it high in the tree for a whole app, or on one AppShell region.

a compact region
<section data-density="compact">
  {/* controls here render at the tighter scale */}
</section>

Persisting the choice without a flash

Read the saved theme and accent from localStorage and stamp them on <html> in a tiny blocking script placed before your app renders. Because it runs before first paint, the user never sees a flash of the wrong theme.

inline before your app (e.g. in <head> or top of <body>)
<script
  dangerouslySetInnerHTML={{
    __html: `(function () {
  try {
    var t = localStorage.getItem("pds-theme");
    if (t === "light" || t === "dark") document.documentElement.dataset.theme = t;
    var a = localStorage.getItem("pds-accent");
    if (a) document.documentElement.dataset.accent = a;
  } catch (e) {}
})();`,
  }}
/>

Then persist on change — from a toggle, or by subscribing to useTheme:

persist on toggle
function persist(theme) {
  document.documentElement.dataset.theme = theme;
  localStorage.setItem("pds-theme", theme);
}
Suppress hydration warnings
Because the pre-hydration script mutates <html>, add suppressHydrationWarning to your <html> element so React does not flag the server/client attribute mismatch.

Overriding tokens

When you need to bend the system — a tenant brand colour, a one-off surface — do it at the semantic layer (--pds-*), never by editing component CSS. Redefine a semantic variable on a scope and everything downstream follows:

scoped override
/* a single panel with a bespoke accent, still AA-managed by you */
.brand-panel {
  --pds-accent: #7c5cff;
  --pds-accent-fg: #ffffff;
}
Do not override these
Never override control radius (sharp corners are the brand) or add box-shadow to controls (the one sanctioned shadow is for floating layers). Never reference the private --pds-p-* primitives — they are not API and can change between releases. Style through the semantic layer only. More in Styling.