whisk
Theming

Dark mode

How Whisk picks a theme and how to pin one.

Whisk supports three modes: automatic, pinned light, pinned dark.

Automatic (the default)

When theme="system" is set on <WhiskProvider> (which is the default), Whisk renders no data-whisk-theme attribute. The widget follows the OS through a prefers-color-scheme media query.

<WhiskProvider config={config}>
  <WhiskSend />
</WhiskProvider>

This route has three things going for it:

  • SSR-safe. Server and client render identical markup; no hydration mismatch.
  • No flash. The browser flips the theme entirely from CSS.
  • No JS. Toggle the OS, the widget re-themes instantly.

Pinned

To override the OS preference, pass theme:

<WhiskProvider config={config} theme="dark">
  <WhiskSend />
</WhiskProvider>

This renders data-whisk-theme="dark" on the widget root. The selector lives at the bottom of the cascade, so it beats the media query.

Driving theme from your app's toggle

Most apps already have a theme system; next-themes, Tailwind's .dark class, a Radix theme. Whisk's stylesheet recognises both contracts, so you can pass theme="system" to Whisk and let your app drive the class on a parent element:

[data-whisk][data-whisk-theme="dark"],
[data-whisk].dark {
  /* dark tokens applied */
}

A typical Tailwind dark-mode setup works without ceremony:

<html className={resolvedTheme === "dark" ? "dark" : ""}>
  <body>
    <WhiskProvider config={config} theme="system">
      <WhiskSend />
    </WhiskProvider>
  </body>
</html>

Customising the dark palette

The dark tokens live inside two selectors: @media (prefers-color-scheme: dark) and [data-whisk][data-whisk-theme="dark"]. To tweak the dark palette, override the same variables documented in Tokens, inside both selectors:

@media (prefers-color-scheme: dark) {
  :root {
    --whisk-bg: #050505;
    --whisk-fg: #f5f5f5;
    --whisk-primary: #00d4ff;
  }
}

[data-whisk][data-whisk-theme="dark"] {
  --whisk-bg: #050505;
  --whisk-fg: #f5f5f5;
  --whisk-primary: #00d4ff;
}

Repeating the block in both selectors is the cost of supporting both "automatic via OS" and "pinned dark." There's no clean way to collapse them in CSS until :is() gets media-query support.

On this page