Skip to main content
← All notes
Building

The theme hook watches a DOM attribute instead of wrapping the app in a provider

software

The site has a dark mode and a light mode. Every component that changes colour between themes needs to know which theme is active. In React, the standard approach is a Context provider — wrap the tree, consume the value, re-render on change. The useTheme hook takes a different path. It reads the data-theme attribute from the document element and subscribes to changes through a MutationObserver. The observer watches one attribute on one element. When the ThemeToggle component sets data-theme to light or dark, the observer callback fires, reads the new value, and updates the hook's state. Every component that calls useTheme gets its own independent subscription to the DOM. No provider wrapping the app. No context value threading through the component tree. No cascading re-renders from a context change at the root. The hook is twenty-seven lines. It initialises the state to dark, reads the current attribute on mount, creates a MutationObserver filtered to the data-theme attribute, and disconnects the observer on cleanup. The attributeFilter option means the observer only fires for the one attribute it cares about — not every class change, not every style update, just theme switches. The ThemeToggle itself is equally simple. It reads the initial theme from localStorage, toggles between dark and light on click, sets the attribute on the document element, and persists the choice. The toggle and the hook never import each other. They communicate through the DOM — the toggle writes the attribute, the hook reads it. This pattern scales without modification. Adding a new themed component means calling useTheme and reading the return value. No provider to update, no context shape to extend, no barrel export to maintain. The DOM is the state bus.

Comments coming soon

Sign in with TikTok to leave a comment. Coming soon.