Skip to content
Devix Open Source

Guide

Getting started

Two steps, and the second one is the important one.

1. The control

npm install @devix-labs/theme-toggle
import { createThemeToggle } from '@devix-labs/theme-toggle';
import '@devix-labs/theme-toggle/styles.css';

createThemeToggle(document.querySelector('#theme'));

That renders a three-way switch — light, dark, system — as a group of real radios, so the arrow keys work and a screen reader announces "2 of 3".

2. The snippet that stops the flash

A page rendered on a server does not know what the visitor chose, so it paints the default and corrects itself a moment later. For anyone using a dark theme that is a white flash on every page load.

The only fix is a blocking script in <head>, before the first paint:

<head>
  <script>
    // 564 bytes, from antiFlashScript()
  </script>
  <link rel="stylesheet" href="/app.css">
</head>

Generate it once, at build time or in your template:

import { antiFlashScript } from '@devix-labs/theme-toggle';

antiFlashScript();                      // the body, as a string
antiFlashScript({ tag: true });         // wrapped in <script>
antiFlashScript({ tag: true, nonce });  // with a CSP nonce
{{-- Laravel --}}
<head>
    {!! \Devix\antiFlash() !!}
</head>
---
import { antiFlashScript } from '@devix-labs/theme-toggle';
---
<head><script is:inline set:html={antiFlashScript()} /></head>

Why this is a string

Because in React it cannot be anything else without trouble. The two most-reacted open issues on the library with nineteen million weekly downloads — seventy-five reactions between them — are that its anti-flash script breaks when React renders it.

A string has no such problem. It is not a component, it does not hydrate, and it runs before your bundle is even fetched.

Making your CSS follow it

By default the theme is written as a class on <html>:

:root { --bg: #ffffff; --fg: #18181b; }
.dark { --bg: #101014; --fg: #ededf0; }

Tailwind's dark: variant works with no configuration. For Bootstrap, or anything using data-theme, say so:

createThemeToggle(el, { attribute: ['class', 'data-bs-theme'] });

Both are applied, which is what a page using two design systems needs — and is still an open request on the alternative.

The theme without the control

import { createTheme } from '@devix-labs/theme-toggle';

const theme = createTheme({ themeColor: { light: '#ffffff', dark: '#101014' } });

theme.get();        // 'system' — what was chosen
theme.resolved();   // 'dark'   — what that means now
theme.set('light');
theme.toggle();     // light → dark → system → light
theme.subscribe((choice, resolved) => {});

Nothing touches the document at import time, so importing this on a server is safe.

Where to go next

Updated 15 Sep 2026