Skip to main content
← All notes
Building

Dark mode without a flash of the wrong theme

softwareaccessibility

The site supports dark and light themes. The toggle is a 44px button fixed to the top right of every page. Click it and the entire site flips — backgrounds, text, accents, glass borders, everything. The trick is making it instant. The ThemeToggle component sets a data-theme attribute on the html element and saves the choice to localStorage. Every colour on the site is a CSS custom property that changes based on that attribute. But if React handled the initial theme, you would see a flash of the default dark theme before the saved preference loaded. So there is a six-line inline script in the document head that reads localStorage and sets data-theme before a single pixel renders. No flash. No hydration mismatch. The theme is correct from the first frame. The other half of the system is the colour pairs. Every accent colour on the site has a dark variant and a light variant. Dark values are the -400 Tailwind shades — vibrant on dark backgrounds. Light values are -600 and -700 shades that pass WCAG AA 4.5:1 contrast on white. A single pickColor function takes a colour pair and the current theme and returns the right shade. The MobileNav uses it for icon colours. The NoteTypeBadge uses it for category labels. The NoteTagPills use it for tag backgrounds. One function, one theme state, and every accent on the site adapts. The useTheme hook watches for changes using a MutationObserver on the html element's attributes. When the ThemeToggle flips the data-theme attribute, the observer fires, and every component that reads the theme re-renders with the correct colours. No context provider, no prop drilling. Just a DOM attribute and a mutation observer.

Comments coming soon

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