1. Documentation
  2. Status page UI
  3. Colour mode
ReadmenpmGitHub
  • Introduction
  • Quickstart
  • Configuration file
  • Monitors
  • Checks and scheduling
  • Notifications
  • Incidents and maintenance
  • Subscribers
  • Metrics and badges
  • Identity and appearance
  • Telemetry
  • The check run
  • Uptime and history
  • Automatic incidents
  • Notification delivery
  • Public subscriptions
  • StatusPage component
  • Themes
  • UI primitives
  • Colour mode
  • React Router
  • Next.js
  • Astro
  • SQLite
  • Cloudflare D1
  • PostgreSQL
  • Schema and migrations
  • Cloudflare Workers
  • Vercel
  • Self-hosting
  • API overview
  • Status endpoints
  • Incident and maintenance endpoints
  • Subscriber endpoints
  • Badges, feeds and metrics
  • Admin endpoints
  • CLI commands
  • Package exports
  • Security
  • Troubleshooting

Colour mode

Light, dark and system, without a flash of the wrong palette.

Colour mode is one attribute. heyo-ui resolves both palettes from a single set of tokens with light-dark(), which reads color-scheme, which is driven by data-mode on <html>. Writing that attribute is the entire switch — there is no class list to keep in sync.

data-modeResult
absentfollow the operating system
lightforce light
darkforce dark

In system mode the attribute is removed, not set to today's resolved value. Freezing the current preference into the document would stop the page following the OS when it changes at sunset.

Configure the initial mode

heyo-status.config.ts
mode: "system", // "light" | "dark" | "system"

Pass it to the page, and the provider mounted by <StatusPage /> uses it as the default:

tsx
<StatusPage snapshot={snapshot} mode={statusPage.mode} />

A visitor's own choice is persisted in localStorage under heyo-status-theme and always wins over the configured default.

Avoid the flash

Render the bootstrap script in the document head, before any paint. It reads the stored preference, falls back to the configured mode and writes the attribute. It is small and CSP-compatible.

import { statusThemeBootstrapScript } from "@heyo-sh/heyo-status/theme/script";<html lang="en" suppressHydrationWarning>  <head>    <script      dangerouslySetInnerHTML={{        __html: statusThemeBootstrapScript(undefined, statusPage.mode),      }}    />  </head>  <body>{children}</body></html>;

statusThemeBootstrapScript(storageKey?, defaultTheme?) — pass undefined for the default key, heyo-status-theme, which is also exported as DEFAULT_STATUS_THEME_STORAGE_KEY.

suppressHydrationWarning on <html>

The script mutates the document before React hydrates. Without suppressHydrationWarning, React reports the attribute it did not render.

The provider and hook

<StatusPage /> mounts StatusThemeProvider itself. Mount it yourself only when building a custom page from the primitives:

tsx
import { StatusThemeProvider, useStatusTheme } from "@heyo-sh/heyo-status/theme/provider";<StatusThemeProvider defaultTheme="system" storageKey="heyo-status-theme">  <CustomStatusPage /></StatusThemeProvider>;
tsx
function ModeSwitch() {  const { theme, resolvedTheme, mounted, setTheme, toggleTheme } = useStatusTheme();  if (!mounted) return null; // the first client render has no stored value yet  return (    <div>      <button onClick={toggleTheme}>{resolvedTheme === "dark" ? "Light" : "Dark"}</button>      <button onClick={() => setTheme("system")} aria-pressed={theme === "system"}>        System      </button>    </div>  );}
theme"system" | "light" | "dark"optional

The stored preference.

resolvedTheme"light" | "dark"optional

What is actually rendered right now.

mountedbooleanoptional

False during the first client render. Gate mode-dependent UI on it to avoid a hydration mismatch.

setTheme(theme: StatusMode) => voidoptional
toggleTheme() => voidoptional

Flips between the two resolved palettes.

A disabled or unavailable localStorage is caught: the toggle keeps working, it simply does not persist.

Built-in toggle

tsx
import { DarkModeToggle } from "@heyo-sh/heyo-status/ui";<DarkModeToggle />;

Already part of StatusHeader, and therefore of every bundled theme.

Related

  • Themes
  • Identity and appearance
UI primitives< PreviousReact RouterNext >

Powered by heyo

On this page

Configure the initial modeAvoid the flashThe provider and hookBuilt-in toggle