Skip to main contentSkip to docs navigation

Color modes

Explore Chassis CSS color modes — the default light mode, dark mode, and how to create custom themes using the data-cx-theme attribute.

Dark mode

Chassis CSS supports color modes — light (default) and dark — controlled via the data-cx-theme attribute. Color modes can be toggled globally on the <html> element, or scoped to individual components and elements.

Alternatively, switch to a media query implementation using the color mode mixin — see the usage section for details. This eliminates the ability to change themes on a per-component basis as shown below.

Example

For example, to change the color mode of a dropdown menu, add data-cx-theme="light" or data-cx-theme="dark" to the parent .dropdown. Now, no matter the global color mode, these dropdowns will display with the specified theme value.

HTML
<div class="dropdown" data-cx-theme="light">
  <button class="button secondary menu-toggle" type="button" id="dropdownMenuButtonLight" data-cx-toggle="menu" aria-expanded="false">
    Default dropdown
  </button>
  <ul class="menu" role="menu" aria-labelledby="dropdownMenuButtonLight">
    <li><a class="menu-item active" href="#">Action</a></li>
    <li><a class="menu-item" href="#">Action</a></li>
    <li><a class="menu-item" href="#">Another action</a></li>
    <li><a class="menu-item" href="#">Something else here</a></li>
    <li><hr class="menu-divider"></li>
    <li><a class="menu-item" href="#">Separated link</a></li>
  </ul>
</div>

<div class="dropdown" data-cx-theme="dark">
  <button class="button secondary menu-toggle" type="button" id="dropdownMenuButtonDark" data-cx-toggle="menu" aria-expanded="false">
    Dark dropdown
  </button>
  <ul class="menu" role="menu" aria-labelledby="dropdownMenuButtonDark">
    <li><a class="menu-item active" href="#">Action</a></li>
    <li><a class="menu-item" href="#">Action</a></li>
    <li><a class="menu-item" href="#">Another action</a></li>
    <li><a class="menu-item" href="#">Something else here</a></li>
    <li><hr class="menu-divider"></li>
    <li><a class="menu-item" href="#">Separated link</a></li>
  </ul>
</div>

How it works

  • As shown above, color mode styles are controlled by the data-cx-theme attribute. This attribute can be applied to the <html> element, or to any other element or Chassis CSS component. If applied to the <html> element, it will apply to everything. If applied to a component or element, it will be scoped to that specific component or element.

  • For each color mode you wish to support, add new overrides for the shared global CSS variables. Chassis already does this in _root.scss for dark mode, with light mode as the default values. To write color mode specific styles, use the mixin:

    SCSS
    // Color mode variables in _root.scss
    @include color-mode(dark) {
      // CSS variable overrides here...
    }
  • Chassis ships a _variables-dark.scss that powers those shared global CSS variable overrides for dark mode. This file isn't required for custom color modes, but it's required for dark mode for two reasons. First, it's better to have a single place to reset global colors. Second, some Sass variables had to be overridden for background images embedded in the compiled CSS for accordions, form components, and more.

Usage

Enable dark mode

Enable the built in dark color mode across your entire project by adding the data-cx-theme="dark" attribute to the <html> element. This will apply the dark color mode to all components and elements, other than those with a specific data-cx-theme attribute applied. Building on the Quick Start template:

HTML
<!doctype html>
<html lang="en" data-cx-theme="dark">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Chassis CSS demo</title>
    <link href="https://cdn.jsdelivr.net/gh/chassis-ui/css/dist/css/chassis.min.css" rel="stylesheet" integrity="sha384-yvuk1frZQQsJ0Fs1SD81+hTLk23vxqDChqHIyeOUeodCKkmBeeKQs0Sy303W7/lx" crossorigin="anonymous">
  </head>
  <body>
    <h1>Hello, world!</h1>
    <script src="https://cdn.jsdelivr.net/gh/chassis-ui/css/dist/js/chassis.bundle.min.js" integrity="sha384-1v5NO74p30+9rrykflgr7I1xyNW4bDvDuiYZQv88sN2iS/F9SJrLBdArVwAroqLS" crossorigin="anonymous"></script>
  </body>
</html>

Chassis CSS does not yet ship with a built-in color mode picker, but you can use the one from the Chassis documentation. Learn more in the JavaScript section.

Building with Sass

The dark mode option is controlled via data attributes instead of media queries and does not automatically toggle the project's color mode. Disable dark mode entirely via Sass by setting $enable-dark-mode to false.

The color-mode() mixin controls how color modes are applied. By default, it uses a data attribute approach, allowing more user-friendly experiences where visitors can choose an automatic dark mode or override their preference (like in the Chassis docs). This approach also scales to additional themes and custom color modes beyond light and dark.

In case you want to use media queries and only make color modes automatic, you can change the mixin's default type via Sass variable. Consider the following snippet and its compiled CSS output.

SCSS
$color-mode-type: data;

@include color-mode(dark) {
  .element {
    color: var(--cx-primary-fg-main);
    background-color: var(--cx-primary-bg-evident);
  }
}

Outputs to:

CSS
[data-cx-theme=dark] .element {
  color: var(--cx-primary-fg-main);
  background-color: var(--cx-primary-bg-evident);
}

And when setting to media-query:

SCSS
$color-mode-type: media-query;

@include color-mode(dark) {
  .element {
    color: var(--cx-primary-fg-main);
    background-color: var(--cx-primary-bg-evident);
  }
}

Outputs to:

CSS
@media (prefers-color-scheme: dark) {
  .element {
    color: var(--cx-primary-fg-main);
    background-color: var(--cx-primary-bg-evident);
  }
}

Custom color modes

While the primary use case for color modes is light and dark mode, custom color modes are also possible. Create your own data-cx-theme selector with a custom value as the name of your color mode, then modify the Sass and CSS variables as needed. Chassis ships a separate _variables-dark.scss to house dark mode Sass variables, but that's not required for custom themes.

For example, you can create a “blue theme” with the selector data-cx-theme="blue". In your custom Sass or CSS file, add the new selector and override any global or component CSS variables as needed. If you're using Sass, you can also use Sass's functions within your CSS variable overrides.

[data-cx-theme="primary"] {
  --cx-fg-color: var(--cx-white);
  --cx-bg-color: var(--cx-primary);
  --cx-border-subtle: var(--cx-primary-30);

  .menu {
    --cx-menu-bg-color: var(--cx-primary);
    --cx-menu-fg-color: var(--cx-primary-contrast);
    --cx-menu-link-fg-color: var(--cx-primary-contrast);
    --cx-menu-link-hover-fg-color: var(--cx-primary-contrast);
    --cx-menu-link-hover-bg-color: var(--cx-primary-40);
    --cx-menu-link-active-bg-color: var(--cx-primary-60);
    --cx-menu-separator-color: var(--cx-primary-40);
  }

  .button.secondary {
    --cx-button-idle-bg-color: transparent;
    --cx-button-idle-fg-color: var(--cx-primary-contrast);
    --cx-button-idle-border-color: var(--cx-primary-contrast);
    --cx-button-hover-bg-color: var(--cx-primary-bg-hover);
    --cx-button-hover-fg-color: var(--cx-primary-fg-hover);
    --cx-button-hover-border-color: var(--cx-primary-contrast);
    --cx-button-press-bg-color: var(--cx-primary-bg-press);
    --cx-button-press-fg-color: var(--cx-primary-fg-press);
    --cx-button-press-border-color: var(--cx-primary-contrast);
  }
}
Example blue theme

Some paragraph text to show how the blue theme might look with written copy.


HTML
<div data-cx-theme="primary">
  ...
</div>

JavaScript

To allow visitors to toggle color modes, create a toggle element that controls the data-cx-theme attribute on the root <html> element. The Chassis docs ship a toggler that initially defers to the system color mode but provides an option to override it and pick a specific mode.

The JavaScript below powers it. Inspect the Chassis docs navbar to see how it's implemented using Chassis components. Include it at the top of the page to reduce screen flickering on reload. When using media queries for color modes, this script may need to be modified or removed if implicit control is preferred.

JavaScript
/*!
 * Color mode toggler for Chassis's docs (https://chassis-ui.com/)
 * Copyright 2011-2025 The Chassis Authors
 * Licensed under the Creative Commons Attribution 3.0 Unported License.
 */

;(() => {
  'use strict'

  const getStoredTheme = () => localStorage.getItem('theme') || 'auto'
  const setStoredTheme = (theme) => localStorage.setItem('theme', theme)

  const getPreferredTheme = () => {
    const storedTheme = getStoredTheme()
    if (storedTheme) {
      return storedTheme
    }

    return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
  }

  const setTheme = (theme) => {
    if (theme === 'auto') {
      document.documentElement.setAttribute(
        'data-cx-theme',
        window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
      )
    } else {
      document.documentElement.setAttribute('data-cx-theme', theme)
    }
  }

  setTheme(getPreferredTheme())

  const showActiveTheme = (theme, focus = false) => {
    const themeSwitcher = document.querySelector('#cxd-theme')

    if (!themeSwitcher) {
      return
    }

    const themeSwitcherText = document.querySelector('#cxd-theme-text')
    const activeThemeIcon = document.querySelector('.theme-icon-active use')
    const buttonToActive = document.querySelector(`[data-cx-theme-value="${theme}"]`)
    const svgOfActiveButton = buttonToActive.querySelector('svg use').getAttribute('href')

    document.querySelectorAll('[data-cx-theme-value]').forEach((element) => {
      element.classList.remove('selected')
      element.setAttribute('aria-pressed', 'false')
    })

    buttonToActive.classList.add('selected')
    buttonToActive.setAttribute('aria-pressed', 'true')
    activeThemeIcon.setAttribute('href', svgOfActiveButton)
    const themeSwitcherLabel = `${themeSwitcherText.textContent} (${buttonToActive.dataset.cxThemeValue})`
    themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)

    if (focus) {
      themeSwitcher.focus()
    }
  }

  window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
    const storedTheme = getStoredTheme()
    if (storedTheme !== 'light' && storedTheme !== 'dark') {
      setTheme(getPreferredTheme())
    }
  })

  window.addEventListener('DOMContentLoaded', () => {
    showActiveTheme(getPreferredTheme())

    document.querySelectorAll('[data-cx-theme-value]').forEach((toggle) => {
      toggle.addEventListener('click', () => {
        const theme = toggle.getAttribute('data-cx-theme-value')
        setStoredTheme(theme)
        setTheme(theme)
        showActiveTheme(theme, true)
      })
    })
  })
})()

Adding theme colors

Adding a new color in $context-colors is not enough for some Chassis components like notifications and lists. New colors must also be defined in $context-colors-fg-main, $context-colors-bg-evident, and $context-colors-border-subtle for light theme; and in $context-colors-fg-main-dark, $context-colors-bg-evident-dark, and $context-colors-border-subtle-dark for dark theme.

This is a manual process because Sass cannot generate its own Sass variables from an existing variable or map. A future version of Chassis will revisit this setup to reduce the duplication.

SCSS
@use "sass:map";
@use "@chassis-ui/css/scss/functions" as *;
@use "@chassis-ui/css/scss/config/defaults" as *;

// Add a custom color to $context-colors
$custom-colors: (
  "custom-color": #712cf9
);
$context-colors: map.merge($context-colors, $custom-colors);

@use "@chassis-ui/css/scss/maps" as *;
@use "@chassis-ui/css/scss/mixins" as *;

// Define color-mode-specific theme maps for the new color

// Light mode
$context-colors-fg-main: map.merge($context-colors-fg-main, ("custom-color": #712cf9));
$context-colors-bg-evident: map.merge($context-colors-bg-evident, ("custom-color": #e1d2fe));
$context-colors-border-subtle: map.merge($context-colors-border-subtle, ("custom-color": #bfa1fc));

// Dark mode
$context-colors-fg-main-dark: map.merge($context-colors-fg-main-dark, ("custom-color": #e1d2f2));
$context-colors-bg-evident-dark: map.merge($context-colors-bg-evident-dark, ("custom-color": #8951fa));
$context-colors-border-subtle-dark: map.merge($context-colors-border-subtle-dark, ("custom-color": #e1d2f2));

// Remainder of Chassis CSS
@use "@chassis-ui/css/scss/root";
@use "@chassis-ui/css/scss/reboot";
// etc

CSS

Variables

Dozens of root level CSS variables are repeated as overrides for dark mode. These are scoped to the color mode selector, which defaults to data-cx-theme but can be configured to use a prefers-color-scheme media query. Use these variables as a guideline for generating your own new color modes.

Sass variables

CSS variables for the dark color mode are partially generated from dark-mode-specific Sass variables in _variables-dark.scss. This includes custom overrides for the colors of embedded SVGs used throughout Chassis components.

Sass mixins

Styles for dark mode, and any custom color modes you create, can be scoped appropriately to the data-cx-theme attribute selector or media query with the customizable color-mode() mixin. See the Sass usage section for more details.

/// Apply styles for a specific color mode (light or dark).
/// When `$color-mode-type` is `"media-query"`, wraps the content in a
/// `@media (prefers-color-scheme: ...)` query, optionally scoped to `:root`.
/// Otherwise, targets a `[data-cx-theme="{mode}"]` attribute selector.
///
/// @param {String} $mode [light] - Color scheme name (`light` or `dark`)
/// @param {Boolean} $root [false] - When `true`, scopes to `:root` inside the media query
/// @content Styles to apply for this color mode
/// @example
///   @include color-mode(dark) { background: black; }
///   // => @media (prefers-color-scheme: dark) { background: black; }
///   //    OR: [data-cx-theme="dark"] { background: black; }
@mixin color-mode($mode: light, $root: false) {
  @if $color-mode-type == "media-query" {
    @if $root == true {
      @media (prefers-color-scheme: $mode) {
        :root {
          @content;
        }
      }
    } @else {
      @media (prefers-color-scheme: $mode) {
        @content;
      }
    }
  } @else {
    [data-cx-theme="#{$mode}"] {
      @content;
    }
  }
}