Dark mode
Dark mode is a full second set of semantic colours, not an inversion. Every surface, line, text and status token has a light and a dark value, defined in the token source and emitted by tokens.css. Co
Dark mode is a full second set of semantic colours, not an inversion. Every surface, line, text and status token has a light and a dark value, defined in the token source and emitted by tokens.css. Components only use semantic tokens, so nothing in a component needs to know which theme is active.
How the theme is chosen
NasaqProvider resolves a preference to light or dark:
| Preference | Meaning |
|---|---|
light | Always light |
dark | Always dark |
system (default) | Follow prefers-color-scheme, live |
It applies the result three ways on <html>, so whichever convention your code uses keeps working:
data-theme="dark"(Nasaq's own selector, used bytokens.css).- The
darkclass (what shadcn's components and Tailwind'sdark:variant expect). color-scheme: dark, so scrollbars and native form controls match.
The choice is saved in localStorage["nasaq-theme"]. Add the theme script to <head> so the saved theme is applied before first paint, with no flash.
Switching
useNasaq() gives you the current and resolved theme and a setter:
import { useNasaq } from "@fadymondy/nasaq/web";
import { Button } from "@fadymondy/nasaq/web";
export function ThemeToggle() {
const { resolvedTheme, setTheme } = useNasaq();
return (
<Button variant="ghost" onClick={() => setTheme(resolvedTheme === "dark" ? "light" : "dark")}>
{resolvedTheme === "dark" ? "Light" : "Dark"}
</Button>
);
}For a ready-made control use ThemeSwitcher (see Switchers), which offers light, dark and system.
If your app already owns the theme (for example with next-themes), pass it in and Nasaq follows:
<NasaqProvider theme={theme} onThemeChange={setTheme}>
{children}
</NasaqProvider>Writing dark-safe styles
- Use semantic tokens (
bg-nq-surface,text-nq-fg,border-nq-line,bg-card,text-muted-foreground). They change with the theme on their own. - Do not write
dark:variants for colours you can express with a token. If you need one, add or reuse a token instead. - Do not use raw hex colours in components. The repo's lint rejects them.
- Surfaces get lighter as they rise in dark mode (page, panel, raised, overlay), and whiter in light mode. A nested layer moves up exactly one level.
- Status colours (
success,warning,danger,info) have their own text and soft-background tokens with contrast checked in both themes.
Contrast
The token package has a test that computes the contrast of every text-on-surface, status and brand-action pair in both themes and fails the build when one is too low. Some brand colours collide in hue with a status colour; those exceptions are listed in the test, and the rule for each is documented in docs/foundations/COLOR.md.
Images and charts
Charts read their series colours from tokens, so they re-colour with the theme. Photographs and illustrations do not change; give logos that need it a dark variant. The official brand marks are rendered by ProductMark from their spec and pick the right colours for the theme.
The token system
Every colour, space, radius, type size and control height in Nasaq is a CSS custom property named --nq-. Components use these and nothing else, which is what lets one component library serve eleven br
RTL and Arabic
Nasaq is built for products that ship in English and Arabic. Right-to-left is a first-class layout, tested in the same stories as left-to-right, not a separate theme.