Theming
Light on pure white, dark on pure black, or the system's choice. Themes switch with one attribute and need no dark variants.
How themes work
Every color is a CSS variable with a light and a dark value. The dark values apply when the system prefers dark, or when an element carries data-theme="dark"; data-theme="light" forces light. Components never use dark: variants: the tokens switch themselves.
Choosing a theme
ThemeProvider remembers the person’s choice (system, light or dark) and sets data-theme on the document. ThemeMenu is the control for it; useTheme() reads and sets it (theme is the choice, resolvedTheme what is shown).
import { Button, ThemeMenu, useTheme } from "@logicplanes/kaeso-ui";
export function ThemeControl() {
return <ThemeMenu />;
}
export function DarkModeOffer() {
const { resolvedTheme, setTheme } = useTheme();
if (resolvedTheme === "dark") return null;
return <Button onClick={() => setTheme("dark")}>Use dark mode</Button>;
}A region in the other theme
Any element can switch its subtree. Overlays opened inside a themed region must portal into it, or they would take the page’s theme; wrap the region in PortalContainer.
import { Button, Menu, PortalContainer } from "@logicplanes/kaeso-ui";
export function DarkPreview() {
return (
<div data-theme="dark" className="bg-panel text-fg">
<PortalContainer>
<Menu>
<Menu.Trigger>
<Button>Actions</Button>
</Menu.Trigger>
<Menu.Content>
<Menu.Item>Rename</Menu.Item>
</Menu.Content>
</Menu>
</PortalContainer>
</div>
);
}Contrast and motion preferences
With prefers-contrast: more, rings move to the strong line and secondary text becomes primary. With prefers-reduced-motion, transforms are removed and only opacity transitions remain.
What not to theme
There is no accent color to change and no radius to tune: the identity is fixed in the tokens. A product that needs a different look forks the tokens, not the components.