Skip to content

Design Systems

Dark Mode That Actually Works — Velocity X's Alpine-Driven Theme Switcher Without FOUC

All articles
🌓 🎨

Inline Script + Tailwind Dark: Mode — System Preference Smarts Included

If your dark mode flickers on page load — white background burning for 200ms before the browser reads localStorage and applies `.dark` to `` — you've hit the Flash of Unstyled Content (FOUC). Your users see a jarring white flash, then the theme kicks in. Velocity X kills this entirely: an inline script in `` executes before the DOM paints, reads localStorage, and applies the theme class before the first pixel renders. No flash. No janky toggle. Just instant, persistent dark mode.

Why FOUC is the #1 Dark Mode Fail

Most theme switchers put their logic in JavaScript that loads after CSS — or worse, in a React hydration effect. The browser paints the page with its default light mode, then your script runs and says "actually, dark mode" and re-paints. Epilepsy-triggering flicker. Even a 100ms delay feels like a bug.

The fix is non-intuitive: move the theme read to an inline script in ``, before any CSS loads. This executes synchronously, blocks paint, and applies the theme class before the first frame. Your users see the correct theme from frame one.

The Velocity X Pattern: Inline Script + Alpine

In `src/layouts/Layout.astro`, before the `` tags:

{`
  
  
`}

This runs before CSS loads. If the user has dark mode saved in localStorage, it adds `.dark` to `` synchronously. If nothing is saved, it checks the system preference via `prefers-color-scheme`. No DOM flicker, no race conditions.

Then, in your Alpine store (Velocity X uses `src/lib/store.ts`):

{`export const store = reactive({
  theme: {
    isDark: document.documentElement.classList.contains('dark'),
    toggle() {
      this.isDark = !this.isDark;
      document.documentElement.classList.toggle('dark');
      localStorage.setItem('theme', this.isDark ? 'dark' : 'light');
    }
  }
});`}

The toggle is one-liner: flip the class, flip the boolean, persist to localStorage. Every component that needs the theme reads `$store.theme.isDark`. Tailwind's `dark:` variants (e.g., `dark:bg-slate-900`) apply automatically. No context providers, no theme context boilerplate.

The Catch: System Preference is a Fallback, Not a Lock

System preference (`prefers-color-scheme`) is excellent for first-time users, but users who toggle your theme switcher expect their choice to persist — even if they change their OS-level preference. Velocity X solves this: localStorage takes precedence. If a user sets dark mode, they stay in dark mode even if their phone switches to light mode at sunrise. Their intent wins.

Frequently Asked Questions

Does the inline script block page load?

No. It's a single, synchronous DOM read/write (microseconds). Blocks paint by milliseconds; humans don't perceive it.

What if JavaScript is disabled?

The page renders in the default light mode, which is a reasonable fallback for the 2% of users without JS.

Can I animate the theme transition?

Yes. Use Tailwind's `transition` utility on your color properties: `bg-white dark:bg-slate-900 transition-colors`. When you toggle the class, Tailwind re-computes and CSS animates the change over 150ms.

How do I handle nested dark mode (e.g., a light card inside dark mode)?

Tailwind's `dark:` variants respect the `.dark` class on ``. For exceptions, override with `dark:dark:bg-white` (no, that doesn't work). Instead, target a parent: `.dark .my-card { @apply bg-white; }` in your CSS, or use a data attribute on the card itself.

Does this work with CSS-in-JS frameworks?

Yes, because you're toggling a class on ``, not managing theme in JavaScript state. Any CSS framework that respects that class (Tailwind, CSS Modules, styled-components) will react automatically.

What about prefers-reduced-motion?

Orthogonal. Handle it separately: `transition-colors` becomes `transition-colors @media (prefers-reduced-motion: no-preference)`. Theme is one axis; motion preference is another.

The Bottom Line

2026 dark modes that flicker on load are a solved problem. Inline script in ``, Alpine store for runtime toggles, localStorage for persistence, system preference as a fallback. Velocity X ships this pattern baked in; you can extend it infinitely. If your dark mode still flashes, you're reading localStorage too late. Move it to ``, ship it, and let your users see the right theme from frame one. For deeper design-system patterns, see /pricing, or dive into the fluid type-scaling that pairs beautifully with theme-aware typography at /blog/fluid-type-scale-clamp-design-tokens.

Let us make some quick suggestions?
Please provide your full name.
Please provide your phone number.
Please provide a valid phone number.
Please provide your email address.
Please provide a valid email address.
Please provide your brand name or website.
Please provide your brand name or website.