Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

GUIDE

Theming

Every dj-* component reads its colors, spacing, type, and sizes from --dj-* custom properties. One stylesheet gives you light, dark, and high-contrast themes. A few overrides make it your brand.

Save Cancel Learn more
Notifications
Live components. The theme buttons set data-dj-theme on the panel; the brand button applies the overrides shown under “Make it your brand.”

Load the theme

Every component carries fallback values and renders without a theme, so this step is optional. @dojo-ng/theme supplies the full token set. Link it once per page, or add @dojo-ng/theme/theme.css to your bundler's global styles. Custom properties inherit through shadow roots, so one page-level stylesheet reaches every component.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@dojo-ng/theme/theme.css">

Switch themes

With no attribute set, the theme follows the operating system's light or dark setting. To choose one, set data-dj-theme on <html> to light, dark, or high-contrast. Components restyle immediately; none of them needs to be told.

// "light", "dark", or "high-contrast". Remove the attribute to follow the OS again.
document.documentElement.dataset.djTheme = "dark";

high-contrast is black on white with 2px input borders and a 3px focus ring, for users who need more than the dark theme's contrast. Windows High Contrast Mode is handled separately, below.

How the tokens are layered

Tokens come in three layers, and each layer refers to the one beneath it. Override at the lowest layer that does the job.

LayerExamplesOverride it to
Primitives--dj-color-primary-600, --dj-color-neutral-0…900, --dj-spacing-medium, --dj-font-size-smallChange the palette or scale everywhere.
Semantic roles--dj-color-text, --dj-color-background, --dj-color-border, --dj-focus-ring-colorChange what a role uses without touching the palette.
Component tokens--dj-button-font-size-medium, --dj-input-height-medium, --dj-loading-linear-heightMake one narrow adjustment.

The dark theme works by remapping primitives. Its neutral scale runs the other way, so --dj-color-neutral-0 becomes the near-black background, and the semantic roles that point at it follow. theme.css lists every token in every theme.

Make it your brand

Load a short stylesheet after theme.css that sets your primary scale. Mirror the selectors theme.css uses, or one of the themes will fall back to the default blue. Your light values belong on :root when no theme is chosen and on [data-dj-theme="light"]. Your dark values belong on [data-dj-theme="dark"] and in the OS dark-mode query. The high-contrast theme is left alone on purpose, since its blues are chosen for contrast.

/* brand.css: load after theme.css */
:root:not([data-dj-theme]),
[data-dj-theme="light"] {
  --dj-color-primary-500: #8b5cf6;
  --dj-color-primary-600: #7c3aed;
  --dj-color-primary-700: #6d28d9;
}

[data-dj-theme="dark"] {
  --dj-color-primary-500: #a78bfa;
  --dj-color-primary-600: #8b5cf6;
  --dj-color-primary-700: #c4b5fd;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-dj-theme]) {
    --dj-color-primary-500: #a78bfa;
    --dj-color-primary-600: #8b5cf6;
    --dj-color-primary-700: #c4b5fd;
  }
}

This shows three steps of the scale. A full brand sets --dj-color-primary-50 through 900, and the dark theme redefines 100, 500, 600, and 700. For a deeper change, copy theme.css, edit it, and load your copy instead. Components need no changes either way.

Make a targeted tweak

Custom properties inherit, so a token set on any element applies only to that element's subtree. Use component tokens on a container or a single element when you want one area to differ:

/* Larger buttons in the toolbar only */
.toolbar {
  --dj-button-font-size-medium: 1.125rem;
  --dj-input-height-medium: 3rem;
}

Theme part of a page

data-dj-theme works on any element, not only <html>, so a dark sidebar or a high-contrast panel takes one attribute. The <dj-theme> element from @dojo-ng/theme does the same as a component, with theme set to light, dark, or auto. auto removes the attribute, so the subtree takes whatever theme surrounds it.

<dj-theme theme="dark">
  <dj-button>Always dark</dj-button>
</dj-theme>

<!-- or, with no element to register -->
<aside data-dj-theme="high-contrast">…</aside>

Reach inside with ::part()

Shadow DOM keeps page CSS out of a component's internals. When a token does not cover what you need, components expose their significant internal elements as parts, which page CSS can style directly. Each component's README lists its parts; dj-button has base, label, and icon. Treat parts as the escape hatch and tokens as the main interface.

dj-button::part(label) {
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

Style validation states

A form control's native <input> lives inside its shadow root, so :invalid cannot reach it from page CSS. Controls such as dj-text-input draw their own invalid state, and every form control also mirrors their validity onto the host element as attributes you can style against: data-dj-required, data-dj-valid, data-dj-invalid, data-dj-user-valid, and data-dj-user-invalid. The user- pair only appears after the user edits and leaves the field, or submits the form, so an untouched field is not flagged.

/* Show a hint only after the user has had a chance to fill the field */
.field:has(dj-text-input[data-dj-user-invalid]) .hint {
  display: block;
}

In browsers that support custom states, the same five names also work as :state(user-invalid) and so on.

Windows High Contrast Mode

When the operating system forces its own colors, theme.css maps the focus ring to the system highlight color and makes overlay backdrops opaque. Each component adds the rest inside its own styles, such as borders where it used shadows and system colors for checked and selected states. There is nothing to configure.

Next: Browse the components 82 custom elements and 25 plugins, each its own package.