Light/Dark Theming

Zero-class automatic theming with light-dark()

The Traditional Approach

Most frameworks require explicit dark mode classes:

<!-- You have to write both -->
<div class="bg-white text-gray-900 dark:bg-gray-900 dark:text-white">
  Content
</div>

This doubles your class count and is easy to forget.

Shift CSS: Zero-Class Theming

Shift CSS uses the native CSS light-dark() function. Components automatically adapt:

<!-- Just write once -->
<div s-surface="raised">
  Content automatically adapts to light/dark mode
</div>

How It Works

Semantic tokens are defined with both light and dark values:

:root {
  color-scheme: light dark;

  --s-surface-base: light-dark(
    var(--s-neutral-50),   /* Light mode */
    var(--s-neutral-950)   /* Dark mode */
  );

  --s-text-primary: light-dark(
    var(--s-neutral-900),
    var(--s-neutral-50)
  );
}

The browser automatically selects the appropriate value based on prefers-color-scheme.

Forcing a Theme

To override the system preference:

/* Force light mode */
:root {
  color-scheme: light;
}

/* Force dark mode */
:root {
  color-scheme: dark;
}

Or with JavaScript:

// Toggle dark mode
document.documentElement.style.colorScheme = 'dark';

Semantic Color Tokens

Token Light Dark Usage
--s-surface-base neutral-50 neutral-950 Default backgrounds
--s-surface-raised neutral-100 neutral-900 Cards, modals
--s-surface-sunken neutral-200 neutral-800 Inputs, wells
--s-text-primary neutral-900 neutral-50 Main text
--s-text-secondary neutral-600 neutral-400 Muted text
--s-border-default neutral-200 neutral-800 Borders

Auto-Contrast Text

Button components automatically calculate readable text color using OKLCH relative color syntax:

[s-btn="primary"] {
  --_bg: var(--s-primary-500);
  /* Auto-contrast: light text on dark bg, dark text on light bg */
  --_color: oklch(from var(--_bg) calc(l < 0.6 ? 0.98 : 0.15) 0.01 h);
}

This ensures WCAG AA compliant contrast regardless of the primary hue chosen.

Search