Skip to main content

Stage App Theming

All Stage apps include built-in support for light and dark mode. The theme system uses CSS custom properties scoped by a data-theme attribute on the document root, with localStorage persistence and OS preference detection.

The useTheme Hook

Every Stage app should use the useTheme hook to manage theme state:

function useTheme(): {
theme: "light" | "dark";
toggle: () => void;
}

Behavior:

  1. On mount, reads localStorage.getItem("shift-theme")
  2. Falls back to OS preference via window.matchMedia("(prefers-color-scheme: dark)")
  3. Sets document.documentElement.setAttribute("data-theme", theme)
  4. Persists changes to localStorage on every toggle

Usage:

function App() {
const { theme, toggle } = useTheme();

return (
<div>
<ThemeToggle theme={theme} toggle={toggle} />
<main>{/* app content */}</main>
</div>
);
}

The ThemeToggle Component

The standard toggle component renders a sun/moon button:

function ThemeToggle({
theme,
toggle,
}: {
theme: "light" | "dark";
toggle: () => void;
}) {
return (
<button
onClick={toggle}
aria-label={`Switch to ${theme === "light" ? "dark" : "light"} mode`}
style={{ background: "none", border: "none", cursor: "pointer", fontSize: "1.2rem" }}
>
{theme === "dark" ? "\u2600\uFE0F" : "\uD83C\uDF19"}
</button>
);
}

Place the toggle in your app's header or navigation bar.

CSS Token Scoping

Theme tokens are defined using [data-theme] selectors, not prefers-color-scheme media queries. This ensures the JavaScript toggle takes precedence over OS settings.

/* Light mode tokens (default) */
:root,
[data-theme="light"] {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #1a1a1a;
--text-secondary: #666666;
--border-color: #e0e0e0;
--accent: #2563eb;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

/* Dark mode tokens */
[data-theme="dark"] {
--bg-primary: #1a1a1a;
--bg-secondary: #2a2a2a;
--text-primary: #f0f0f0;
--text-secondary: #a0a0a0;
--border-color: #333333;
--accent: #60a5fa;
--shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
Dark ModeLight Mode
Stage app in dark modeStage app in light mode

Reference tokens in your components:

.card {
background: var(--bg-secondary);
color: var(--text-primary);
border: 1px solid var(--border-color);
box-shadow: var(--shadow);
}

Palette Integration

When your app uses a Palette theme, the token variables are automatically populated from the palette's design tokens. Your CSS should always reference var(--token-name) rather than hard-coded colors to ensure palette compatibility.

Requirements

The theme toggle is mandatory for all new Stage apps. Your app must:

  1. Include the useTheme hook and ThemeToggle component
  2. Use CSS custom properties for all colors, shadows, and theme-dependent values
  3. Support both light and dark modes without layout breakage
  4. Persist the user's choice via localStorage (shift-theme key)

Retryable Bootstrap States

Stage apps must handle all bootstrap states gracefully. Never gate on if (!resource) return null. Instead, always render a meaningful UI for each state:

StateUI
loadingSpinner or skeleton with status text
unauthenticatedLogin prompt with clear CTA
readyFull app UI
errorError message with retry button

Every error state should include a retry CTA that re-triggers the failed operation.