Skip to main content

Enable light and dark themes, choose semantic colors, and apply an optional host accent.

3 min read

Semantic color tokens let a component keep the same classes in light and dark themes. Mount the shared theme provider, then choose colors by their purpose rather than their current appearance.

Enable theme switching

Installation mounts the provider with <AppShell theme>. Its default choice is system; ThemeProvider resolves the operating system preference and applies .dark to the document when needed. An explicit choice is saved under tale-theme in local storage.

tsx
import { ThemeSwitcher } from '@tale/ui/theme-switcher';

export function AppearanceControl() {
  return <ThemeSwitcher />;
}

Try the theme control in this page's header. Choose Dark, then Light, and compare the swatches below. Choose System to follow the operating system again.

Live example

Canonical family

  • bg-bg-base
  • bg-bg-elevated
  • bg-bg-muted
  • bg-accent-base
  • bg-success-bg
  • bg-warning-bg
  • bg-danger-bg
  • bg-info-bg

HSL family

  • bg-background
  • bg-card
  • bg-muted
  • bg-primary
  • bg-secondary
  • bg-destructive
  • bg-success
  • bg-warning

ThemeSwitcher defaults to a menu. Its segmented variant presents the choices inline. Both must live inside the provider tree.

Read the choice or the displayed result

tsx
import { useTheme } from '@tale/ui/theme';

export function ThemeSummary() {
  const { theme, resolvedTheme, setTheme } = useTheme();
  return (
    <button type="button" onClick={() => setTheme('system')}>
      Preference: {theme}; displayed theme: {resolvedTheme}
    </button>
  );
}

theme is light, dark, or system. resolvedTheme is the resulting light or dark. Use the resolved value when selecting an image or chart palette. Querying prefers-color-scheme independently would ignore an explicit user choice.

The Tailwind dark: variant follows the .dark class. The provider also updates CSS color-scheme and briefly suppresses transitions during a switch. This avoids animating every color on the page at once.

Choose a token family consistently

The stylesheet exposes two supported families:

FamilyExample surfaceExample secondary text
Canonical semantic tokensbg-bg-base text-fg-base border-border-basetext-fg-muted
HSL-compatible aliasesbg-background text-foreground border-bordertext-muted-foreground

Follow the surrounding component's vocabulary. Both resolve through the shared stylesheet; neither requires a second set of light and dark classes at every call site. Colours maps common tokens to their uses.

When extending the token set, define the light and dark values together. Check the actual foreground/background pairing, including hover, disabled, error, and focus states. A semantic name does not by itself prove sufficient contrast.

Apply a host accent

AccentColorProvider supplies a runtime accent to components that opt into it, including route-tab indicators and selected sub-panel rows:

tsx
import { AccentColorProvider } from '@tale/ui/accent-color';

<AccentColorProvider accentColor="#056CFF">
  {children}
</AccentColorProvider>;

This is a composition excerpt: the host supplies children and a validated color. Without the provider, participating components use their default treatment. The context does not recolor every component or replace all theme tokens; for example, Tabs uses its stylesheet classes directly.

Keep organization lookup and branding policy in the service. Review an accent on both themes before using it for a meaningful indicator.

Keep browser assets in sync

ThemeAssets, mounted inside the theme tree, updates the favicon and theme-color metadata to match an explicit theme choice. Your HTML must provide the elements it updates: favicon-light, favicon-dark, theme-color, and theme-color-dark.

If the page changes theme but its browser tab icon does not, inspect those IDs and the asset URLs. If only part of a page changes, look for hardcoded colors or an extra theme provider. Use the accessibility checks to verify contrast and reduced-motion behavior in the rendered page.