The one-button light/dark switch you drop in a navbar, header or settings row — click it and the whole app flips theme, and the choice survives a reload. Reach for it on any site that has a dark mode at all: the header of a marketing or docs site, an app shell or dashboard sidebar, a settings or appearance page, an admin panel, a developer tool or playground, a blog, and the top-right corner of more or less every React starter. Common asks it answers: "dark mode toggle", "theme toggle button", "light dark switcher", "dark mode toggle react", "theme switcher react", "toggle dark mode tailwind", "tailwind dark class toggle", "sun moon toggle button", "dark mode without next-themes", "next-themes alternative", "theme toggle shadcn", "shadcn dark mode toggle component", "shadcn mode toggle", "nextjs dark mode toggle", "vite react dark mode", "react router dark mode", "astro dark mode toggle", "remember user theme localStorage", "respect prefers-color-scheme", "system theme default dark mode", "dark mode flash on page load", "FOUC dark mode nextjs", "hydration mismatch dark mode", "window is not defined dark mode", "toggle dark class on html element", "dark mode toggle without provider", "dark mode toggle accessibility aria-pressed", "ダークモード 切り替え react", "テーマ切り替えボタン", "ダークモード 初回表示 ちらつき", "システム設定に追従 ダークモード". shadcn/ui has no installable toggle, and the measurement is unambiguous: across all sixty-three registry entries (sixty-two fetchable today) the only component that touches theming at all is sonner, which calls next-themes' useTheme to decide what colour to paint a toast — it reads the theme, it does not switch it. The official dark-mode guide is a guide: it hands you a next-themes provider to wire up and a dropdown to assemble yourself, so the button itself is written by hand every time. This is that button — one file, no provider, no context, no next-themes, no extra package. It toggles the `dark` class on the html element, which is exactly what Tailwind's class dark mode and the shadcn tokens already read, so it works with the theme you have rather than introducing another one. On first load it reads the saved choice from localStorage and falls back to the OS `prefers-color-scheme`, so a first-time visitor gets their system theme and a returning one gets their own; every later click writes the choice back. That first read happens in an effect rather than during render, because `window` does not exist on the server and an inline branch would either crash SSR or hydrate to different markup than it sent — which is the `window is not defined` / hydration-mismatch pair that the hand-written version hits first. It also means the theme is applied just after first paint, so add the usual one-line script in your document head if you need to kill the flash on a static page. The Sun/Moon swap is done with the `dark:` variant rather than JS state, so the icon matches the document even if something else on the page changes the theme. It is a real button that forwards every button prop (className, id, onClick, disabled), carries an aria-label and an aria-pressed that reflects the current mode, hides both icons from screen readers, and has a focus-visible ring; styling uses shadcn tokens (accent, muted-foreground, ring, border) so it matches your other icon buttons. Within pulld it sits with the other things that live in a header: kbd and keyboard-shortcuts, command-palette, network-status and announcement-bar. lucide-react is the only dependency.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/theme-toggle.json"