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

TokenLightDarkUsage
--s-surface-baseneutral-50neutral-950Default backgrounds
--s-surface-raisedneutral-100neutral-900Cards, modals
--s-surface-sunkenneutral-200neutral-800Inputs, wells
--s-text-primaryneutral-900neutral-50Main text
--s-text-secondaryneutral-600neutral-400Muted text
--s-border-defaultneutral-200neutral-800Borders

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