v1.6.x-dev NDS IQ v6

Themes - National Design System

Keep the DGA default, switch on dark mode, or make the system your own. Define a brand palette from a few seed colours with the OKLCH engine, or ship a full stylesheet theme that overrides the design tokens.

Theme Switcher

Everything lives in one attribute on the root element: data-theme is a space-separated token list holding the mode (dark, with light as the default) and an optional theme name, so data-theme="dark crimson" is dark mode on the Crimson palette. Give any control a data-theme-value and it becomes a switcher: clicking it writes that theme token into data-theme (keeping the dark token), saves the choice, and syncs aria-current. Pick a theme and every component below updates.

Themed link Status
<div class="nds-dropmenu"> <button class="nds-btn nds-secondary-outline nds-dropmenu-trigger"> <span class="nds-label">Theme</span> </button> <div class="nds-dropmenu-menu nds-theme-menu" hidden> <div class="nds-dropmenu-scroll"> <button class="nds-btn nds-subtle nds-dropmenu-item" data-theme-value=""> <span class="nds-label">Default (DGA)</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-theme-value="crimson"> <span class="nds-label">Crimson</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-theme-value="corporate"> <span class="nds-label">Corporate</span> </button> <button class="nds-btn nds-subtle nds-dropmenu-item" data-theme-value="sunset"> <span class="nds-label">Sunset</span> </button> </div> </div> </div>

Dark Mode

Add data-theme-toggle to any button or switch and it is wired automatically: the click flips the dark token (keeping any active theme), persists the choice, swaps the moon and sun icon, and plays a circular reveal. Dark works on every palette.

Button toggle
<button class="nds-btn nds-subtle nds-icon-only" data-theme-toggle aria-pressed="false" aria-label="Toggle dark mode"> <i class="nds-icon nds-hgi-moon-02" aria-hidden="true"></i> </button>
Switch toggle (settings pages)
<div class="nds-form-container nds-switch-container" data-theme-toggle> <div class="nds-form-header" data-feedback-target> <label for="dark-switch"> <span class="nds-label">Dark Mode</span> <span class="nds-info">Switch between light and dark themes</span> </label> </div> <div class="nds-form-control"> <div class="nds-switch nds-neutral"> <input type="checkbox" id="dark-switch" class="nds-switch-input" role="switch" aria-label="Dark mode"> <div class="nds-switch-track"><div class="nds-switch-thumb"></div></div> </div> </div> </div>

Custom Palette

Define a complete theme from one to three seed colours. The OKLCH engine derives every ramp step from your seed with CSS relative colour, oklch(from var(--brand-primary) L C H): it keeps the seed's hue, sets a perceptually-even lightness, and scales the chroma, so the ramp stays balanced for any colour. Set the seeds plus the data-palette flag on the root element and the full light palette, the brand-tint alphas, a temperature-matched neutral scale, and the dark variant are all generated. Without the flag the page stays on the frozen DGA default. The engine needs CSS relative colour (Chrome 119, Safari 16.4, Firefox 128; mid-2024); older browsers fall back to the DGA default, so ship a stylesheet theme if you must brand them.

Themed link Status
<!-- Seeds inline on the root element --> <html data-palette style="--brand-primary: #7c3aed; --brand-secondary: #ec4899; --neutral-tint: 0.4;">

Stylesheet Themes

For exact, contrast-checked colours, the widest browser support, or styling beyond colour (custom hero layouts, patterns, injected content), ship a theme as its own stylesheet that overrides the colour tokens at :root. In production you load it server-side as a render-blocking <link> after the NDS critical CSS. Override the full set of themeable tokens below; the semantic and component tokens re-resolve from them.

Override the colour tokens in your own stylesheet
/* DGA values shown; replace with your brand ramp. Status + base stay on DGA. */ :root { /* Primary ramp (anchor your brand colour at the 600 step) */ --colors-primary-25: #f7fdf9; --colors-primary-50: #f3fcf6; --colors-primary-100: #dff6e7; --colors-primary-200: #b8eacb; --colors-primary-300: #88d8ad; --colors-primary-400: #54c08a; --colors-primary-500: #25935f; --colors-primary-600: #1b8354; --colors-primary-700: #166a45; --colors-primary-800: #14573a; --colors-primary-900: #104631; --colors-primary-950: #092a1e; /* Brand-tint alphas (chips, table selection, footer) */ --colors-primary-alpha-10: #1b835419; --colors-primary-alpha-20: #1b835433; --colors-primary-alpha-30: #1b83544c; --colors-primary-alpha-40: #1b835466; --colors-primary-alpha-50: #1b83547f; --colors-primary-alpha-60: #1b835499; --colors-primary-alpha-70: #1b8354b2; --colors-primary-alpha-80: #1b8354cc; --colors-primary-alpha-90: #1b8354e5; /* Secondary ramp */ --colors-secondary-25: #fffef7; --colors-secondary-50: #fffef2; --colors-secondary-100: #fffce6; --colors-secondary-200: #fcf3bd; --colors-secondary-300: #fae996; --colors-secondary-400: #f7d54d; --colors-secondary-500: #f5bd02; --colors-secondary-600: #dba102; --colors-secondary-700: #b87b02; --colors-secondary-800: #945c01; --colors-secondary-900: #6e3c00; --colors-secondary-950: #472400; /* Tertiary ramp (anchor at the 500 step) */ --colors-tertiary-25: #fefcff; --colors-tertiary-50: #f9f5fa; --colors-tertiary-100: #f2e9f5; --colors-tertiary-200: #e1cce8; --colors-tertiary-300: #ccadd9; --colors-tertiary-400: #a57bba; --colors-tertiary-500: #80519f; --colors-tertiary-600: #6d428f; --colors-tertiary-700: #532d75; --colors-tertiary-800: #3d1d5e; --colors-tertiary-900: #281047; --colors-tertiary-950: #16072e; --colors-tertiary-alpha-10: #80519f19; --colors-tertiary-alpha-20: #80519f33; /* Neutral ramp (grayscale; adds 750 + 850 steps) */ --colors-neutral-25: #fcfcfd; --colors-neutral-50: #f9fafb; --colors-neutral-100: #f3f4f6; --colors-neutral-200: #e5e7eb; --colors-neutral-300: #d2d6db; --colors-neutral-400: #9da4ae; --colors-neutral-500: #6c727e; --colors-neutral-600: #4d5761; --colors-neutral-700: #384250; --colors-neutral-750: #2b3643; --colors-neutral-800: #1f2a37; --colors-neutral-850: #18212f; --colors-neutral-900: #111927; --colors-neutral-950: #0c111b; /* Deep brand surfaces (hero / footer / image overlay) */ --background-primary-strong: var(--colors-primary-900); --background-primary-light: var(--colors-primary-50); --img-overlay-color: var(--colors-neutral-950); } /* Dark-mode corrections; double the :root if this sheet loads first. */ :root:root[data-theme~="dark"] { --background-card: #1f2a37; --background-footer: #0c111b; }

Built-in Features

Auto-initialization

Toggles and switchers wire themselves: drop in data-theme-toggle or data-theme-value controls and they work, no setup code.

Preference Persistence

The chosen mode and theme are saved to localStorage and restored on every page, so the choice follows the user across visits.

Zero-Flash

A saved mode or theme re-applies before the page is visible, so there is no colour flash on first load or return visit.

Circular Reveal Animation

Switching mode or theme expands the new look outward from the clicked control in a circular ripple, with an instant swap where View Transitions are unsupported.

Light and Dark

A built-in dark mode that works on every palette, with an automatic moon and sun icon swap on any toggle control.

Custom Palettes

Set one to three seed colours and a neutral-tint dial; the OKLCH engine derives the full ramp and the dark variant. Dark follows the seeds with no extra work.

Stylesheet Themes

Ship a full theme as its own stylesheet that overrides the colour tokens at the root, for bespoke palettes and visuals beyond the generated ramp.

Programmatic Control

Read or flip the mode with NDS.Theme.get/set/toggle, or apply a custom palette by setting --brand-* and the data-palette flag.

Usage Guidelines

Best Practices

  • Most government sites need no setup: the DGA default is active out of the box. Add theming only when you need dark mode or your own brand colours
  • Place a dark-mode button toggle in the Top Bar so it is reachable from every page; use the switch toggle on settings or preferences pages
  • Reach for a custom palette (data-palette + --brand-primary) when you want your brand colour fast and dark mode handled for you
  • Reach for a stylesheet theme when you need exact, contrast-checked colours, support for older browsers, or styling beyond colour (layouts, patterns, injected content); in production load it server-side as a render-blocking <link>, rather than injecting at runtime
  • Use NDS.Theme.toggle() / NDS.Theme.set() for mode changes instead of writing data-theme by hand, so the active theme token is preserved
  • Reference semantic tokens (--background-card, --text-default) in your own component CSS so it follows both mode and theme automatically
  • Do not hardcode colours in your CSS: hardcoded values respond to neither dark mode nor themes
  • For a custom palette, check WCAG contrast (4.5:1 text, 3:1 UI) on your primary against white. The seed anchors the 600 step exactly, but the derived steps are perceptual approximations and are not contrast-guaranteed
  • Scope your own dark overrides with :root[data-theme~="dark"]: the word selector matches even when a theme token is also present, such as "dark crimson"
  • Test every component in both modes and across your themes whenever you add a surface

Data Attributes

AttributeDescription
data-theme-togglePlace on any button or switch container to register it as a dark-mode toggle. Keeps aria-pressed, the moon and sun icon, and the checkbox state in sync. Flips only the dark token, preserving any active theme.
data-themeSet on the root element. A space-separated token list: the mode (dark; light is the default, no token) plus an optional theme name. Matched per token by the ~= word selector, so they coexist.
data-theme-valueSet on a switcher item. On click the matching theme token is written into data-theme (preserving the dark token), the choice is saved, and aria-current syncs. An empty string restores the DGA default.
data-palette + --brand-*Set on the root element to activate the runtime OKLCH ramp from your inline seed variables (--brand-primary required; -secondary, -tertiary, --neutral-tint, --nds-font-brand, --font-weight-{regular,medium,semibold,bold} optional). With no flag the page is pure DGA.
data-seed-*Place on a switcher item (alongside data-theme-value) to apply a custom palette on click: data-seed-primary (required), plus optional data-seed-secondary, data-seed-tertiary, data-seed-tint, data-seed-font, data-seed-weight-{regular,medium,semibold,bold} (for a brand font that reads lighter or heavier than IBM Plex at the same nominal weight). The switcher sets the matching --brand-* seeds plus data-palette, then persists the palette so it restores on the next visit.
data-theme-cssOn a switcher item, the URL of a stylesheet theme's CSS, so a live multi-theme picker can load it on demand and unload it on switch-away. A single-theme production site links the stylesheet server-side instead.
data-theme-jsOn a switcher item (alongside data-theme-css), the URL of a JS event-pack for a stylesheet theme. Loaded once on first activation and re-executed on each switch-in so the theme can wire its own behaviour. Read by nds-theme.js at both click-time and reconciliation.

CSS Custom Properties

Set these on the root element (with data-palette) for a custom palette, or at :root in a stylesheet theme.

PropertyDefaultDescription
--brand-primaryDGA greenSeed for the primary colour family (anchors at the 600 step). Drives the OKLCH ramp.
--brand-secondaryderivedSeed for the secondary family. Auto-derived from primary if omitted.
--brand-tertiaryderivedSeed for the tertiary family (anchors at the 500 step). Auto-derived if omitted.
--neutral-tint1Gray temperature dial (0 = true gray). Warms or cools the gray scale toward the primary hue.
--nds-font-brandIBM Plex Sans ArabicTheme typeface stack, e.g. 'Cairo', sans-serif. The font must be loaded by the page.
--font-weight-regular400Regular weight for the theme font. Set per-theme in _register.scss when the brand font reads lighter than IBM Plex at the same nominal weight (corporate and sunset themes use 300).
--font-weight-medium500Medium weight for the theme font.
--font-weight-semibold600Semibold weight for the theme font.
--font-weight-bold700Bold weight for the theme font.
--background-primary-strong--colors-primary-900Deep brand surface used by the hero and footer.
--background-primary-light--colors-primary-25Light brand surface token set by the OKLCH ramp and also by full-override themes (such as Hajj). Used for subtle tinted backgrounds.
--img-overlay-color--colors-primary-950Colour of the translucent overlay on hero images.
--colors-*DGA paletteThe full colour ramps (primary, secondary, tertiary, neutral, status). Override these directly in a stylesheet theme for exact values.

JavaScript API

The NDS.Theme module initializes automatically and owns both axes. Use it to read or flip the dark and light mode; predefined themes are applied by the switcher (or by writing the data-theme token), and a custom palette by setting the seeds plus data-palette.

// ── Dark / light MODE ────────────────────────────────────────── NDS.Theme.get(); // 'light' | 'dark' (reads the mode token) NDS.Theme.set('dark'); // no animation origin (center of screen) NDS.Theme.set('light', el); // circular reveal originating from an element NDS.Theme.toggle(el); // flip mode, preserving the active theme token NDS.Theme.init(); // re-init after injecting new toggles/switchers // ── Apply a custom palette ───────────────────────────────────── // Set the seeds (primary required; the rest auto-derive) + the flag that // gates the runtime OKLCH ramp. The whole palette and dark mode re-resolve. const root = document.documentElement; root.style.setProperty('--brand-primary', '#7c3aed'); root.style.setProperty('--neutral-tint', '0.4'); root.setAttribute('data-palette', ''); // ── Restore the DGA default ──────────────────────────────────── ['--brand-primary', '--brand-secondary', '--brand-tertiary', '--neutral-tint'] .forEach(p => root.style.removeProperty(p)); root.removeAttribute('data-palette'); // frozen DGA palette takes over
Last Modified Date: 03/07/2026 - 05:43 PM
Was this page useful?
60% of users said Yes from 2843 Feedbacks