Dark mode
newt/ui is dark by default and ships an opt-in light surface set. Wiring a toggle is three lines of CSS and one attribute.
The library’s default palette is dark, which is the whole point of a
Discord-inspired system. The light palette is an override on the same token
names, selected by data-newt-theme="light" or the .newt-light class, so no
component has to know which one is active.
The attribute
<!-- dark: the default, nothing to set -->
<html>
<!-- light -->
<html data-newt-theme="light"></html>
</html>
Set color-scheme alongside it so form controls, scrollbars and the caret
follow:
document.documentElement.dataset.newtTheme = theme
document.documentElement.style.colorScheme = theme
Avoid the flash
A theme read from storage after hydration paints the wrong palette first. Read
it in a blocking inline script in <head>, before the first paint:
<script>
try {
const theme = localStorage.getItem("newt-ui:theme")
if (theme === "light" || theme === "dark") {
document.documentElement.dataset.newtTheme = theme
document.documentElement.style.colorScheme = theme
}
} catch {}
</script>
This site does exactly that. Its own copy is the inline head script in
apps/docs/astro.config.ts, which resolves the stored value before the first
paint and writes it to data-newt-theme.
Next.js
next-themes handles the storage, the system preference and the inline script.
Point it at the attribute the palette uses:
"use client"
import { ThemeProvider } from "next-themes"
export function Providers({ children }: { children: React.ReactNode }) {
return (
<ThemeProvider
attribute="data-newt-theme"
defaultTheme="dark"
enableSystem={false}
>
{children}
</ThemeProvider>
)
}
"use client"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
export function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<Button
variant="secondary"
aria-label={theme === "dark" ? "Switch to light" : "Switch to dark"}
onClick={() => setTheme(theme === "dark" ? "light" : "dark")}
>
{theme === "dark" ? "Light" : "Dark"}
</Button>
)
}
Vite
No library needed — the whole toggle is the attribute plus storage:
const KEY = "newt-ui:theme"
export type Theme = "dark" | "light"
export function applyTheme(theme: Theme): void {
document.documentElement.dataset.newtTheme = theme
document.documentElement.style.colorScheme = theme
localStorage.setItem(KEY, theme)
}
export function readTheme(): Theme {
return localStorage.getItem(KEY) === "light" ? "light" : "dark"
}
Call applyTheme(readTheme()) from the inline script in index.html, and again
from your toggle.
Nuxt
@nuxtjs/color-mode writes the attribute for you and injects the inline script:
export default defineNuxtConfig({
modules: ["@newtui/nuxt", "@nuxtjs/color-mode"],
colorMode: {
preference: "dark",
fallback: "dark",
classSuffix: "",
dataValue: "newt-theme",
},
})
dataValue is what produces data-newt-theme on <html>; without it the module
writes a class and the palette never matches.