← Back to all insights
Frontend

Dark mode should feel alive

Most theme toggles are invisible. Here is why that is a missed opportunity - and how to fix it.

Angus Uelsmann Angus Uelsmann 3 min read

Most dark mode toggles instantly swap colors. It works. But it feels dead. One frame: light. Next frame: dark. No continuity. No acknowledgment that something changed. The interface just snaps.

Interfaces communicate through behavior, not just appearance.

What you remove matters more than what you add.

Core claim

  • Instant transitions break continuity.
  • Interfaces should acknowledge state changes.
  • Motion is not decoration - it is communication.

The problem with instant

An instant color swap is technically correct. The theme changes. The function works.

But interfaces are not just function. They are experience.

A hard cut exposes the mechanism instead of hiding it.

It breaks immersion. It draws attention to the seam between states instead of smoothing over it. The user becomes aware of the mechanism rather than just enjoying the result.

Compare it to any well-designed transition in a native app. Settings panels slide. Modals fade. Selection states ripple.

These are not decorations. They are the product communicating that something happened - and that it is under control.

Interfaces should guide, not negotiate.

A theme toggle should do the same.

What I wanted

I wanted the theme change to feel like part of the product. Not an interruption. Not a flash.

Something that originates from the button you just pressed and expands outward - like a decision taking effect.

I had seen the View Transition API mentioned in browser release notes but had not found a real use case for it. A theme toggle turned out to be the perfect fit.

The API lets you snapshot the current and next states of the page, then animate between them.

With a clip-path circle anchored to the toggle button, the new theme ripples outward from exactly where you clicked - expanding until it covers the viewport.

Good defaults are invisible decisions.

Duration: 620ms. Easing: ease-in-out. It feels intentional because it is.

The details that matter

The wave is the main event, but there are four other things that make it work properly.

Image switching. If your light and dark themes use different images, a raw color swap leaves the wrong image visible.

Nightwave checks for data-theme-dark and data-theme-light attributes and swaps src, poster, or background-image automatically. The wave covers the transition.

Reduced motion. Not everyone wants animation.

Nightwave checks prefers-reduced-motion before every wave. If enabled, the animation is skipped. The theme still changes instantly using CSS transitions. Nothing breaks.

System sync. If there is no saved preference, Nightwave reads prefers-color-scheme and applies the correct theme.

It also listens for changes. If the OS theme changes mid-session, the interface follows - without reload.

Flash prevention. Without a small script in <head>, users see a flash of light on every load.

Nightwave reads localStorage before first paint and sets data-theme. No flash. No transition glitch.

Small details shape perception more than large features.

Zero dependency, by choice

Nightwave is a single JavaScript file. No npm. No bundler. No build step.

Drop it in, link the stylesheet, call init(). Done in under a minute.

I wanted something that works everywhere - from full frameworks to static HTML.

Constraints create clarity.

The result is 2 KB of code that does one thing well.

Get it

Nightwave is available as a one-time purchase. Full source, no lock-in.

JS Module Nightwave Zero-dependency wave theme toggle - 2 KB, no build step, works everywhere.
€9 one-time Get Nightwave

Try it yourself: the toggle in the top-right corner of this page runs on Nightwave. Full documentation at angusu.de/docs/nightwave.

Found this useful? Support the work →

If this is the kind of thinking you want in your product, say hello.

Start the conversation.