FOUNDATIONS
Theming
Three orthogonal data-attributes on <html> drive every visual choice. Switch one without touching the others, define your own variants on top of the lib's neutral defaults, and persist the user's preference across reloads.
The three switchers
Damo UI reads three independent attributes from the document root. They cascade through every component because the design tokens are CSS variables.
1<html data-theme="light" data-palette="default" data-density="normal">
2 <body>...</body>
3</html>
data-theme
Light / Dark
Toggles surfaces, foreground, and Memphis chrome. The lib ships light only — you declare the dark override.
data-palette
Brand variants
No built-in values — declare any list you want (default, sunset, cyberpunk, forest…) and override --primary & friends per palette.
data-density
compact / normal / comfortable
Multiplies vertical spacing via --density-scale-y. Lib ships these three values out of the box.
1. Adding dark mode
Re-declare every public token under [data-theme='dark']. You only need to override the semantic layer — components consume those, never the raw scales directly.
1/* app/globals.css */
2:root[data-theme='dark'] {
3 --background: #09090b;
4 --foreground: #fafafa;
5 --card: #18181b;
6 --card-foreground: #fafafa;
7 --muted: #27272a;
8 --muted-foreground: #a1a1aa;
9
10 --primary: #fafafa;
11 --primary-foreground: #18181b;
12 --secondary: #27272a;
13 --secondary-foreground: #fafafa;
14
15 --border: #27272a;
16 --memphis-shadow-color: #fafafa;
17 --memphis-border-color: #fafafa;
18}
19
The full list of tokens you can override lives in Colors (semantic) and Tokens (architecture).
2. Adding custom palettes
Same pattern, different attribute. You can scope a palette to a single mode by chaining attribute selectors.
1/* app/globals.css — define a custom palette */
2:root[data-palette='cyberpunk'] {
3 --primary: #0f766e;
4 --primary-foreground: #ffffff;
5 --secondary: #7c4dff;
6 --secondary-foreground: #ffffff;
7}
8
9/* Combine palette + dark mode by chaining the selectors */
10:root[data-theme='dark'][data-palette='cyberpunk'] {
11 --background: #170731;
12 --foreground: #14b8a6;
13}
14
3. Density scale
Density flips a single CSS variable that components multiply into their vertical padding. You can change the multipliers globally if the defaults don't suit your product.
1:root { --density-scale-y: 1 } /* normal (default) */
2:root[data-density='compact'] { --density-scale-y: 0.75 }
3:root[data-density='comfortable'] { --density-scale-y: 1.25 }
4
4. Drop-in switcher components
The lib ships three controls that read & write the attributes and persist to localStorage. Pop them anywhere — the navbar is the most common home.
1import { AttrToggleGroup } from 'damo-ui'
2
3export function ThemeBar() {
4 return (
5 <div className="flex gap-3 items-center">
6 <AttrToggleGroup
7 label="Theme"
8 storageKey="theme"
9 attribute="data-theme"
10 options={[
11 { value: 'light', label: 'Light' },
12 { value: 'dark', label: 'Dark' },
13 ]}
14 defaultValue="light"
15 />
16 <AttrToggleGroup
17 label="Palette"
18 variant="select"
19 storageKey="palette"
20 attribute="data-palette"
21 options={[
22 { value: 'default', label: 'Plum + Gold' },
23 { value: 'cyberpunk', label: 'Cyberpunk' },
24 ]}
25 defaultValue="default"
26 />
27 <AttrToggleGroup
28 label="Density"
29 storageKey="density"
30 attribute="data-density"
31 options={[
32 { value: 'compact', label: 'Compatta' },
33 { value: 'normal', label: 'Normale' },
34 { value: 'comfortable', label: 'Ampia' },
35 ]}
36 defaultValue="normal"
37 />
38 </div>
39 )
40}
41
Full props in AttrToggleGroup.
5. Programmatic switching
If the built-in components don't fit (custom UI, settings page, system-preference media query, A/B test…) flip the attributes by hand:
1// Read & write any of the three attributes from JS:
2const html = document.documentElement
3
4html.dataset.theme = 'dark' // applies dark vars
5html.dataset.palette = 'cyberpunk' // applies cyberpunk palette overrides
6html.dataset.density = 'compact' // applies density scale
7
8// Persist for next visit:
9localStorage.setItem('theme', 'dark')
10localStorage.setItem('palette', 'cyberpunk')
11localStorage.setItem('density', 'compact')
12
6. Preventing the flash
Server-rendered apps render the default theme first, then React hydrates the persisted choice. To avoid the flash of unstyled-theme (FOUT), restore the attributes from localStorage in a blocking script before hydration.
1// app/layout.tsx — prevent flash of incorrect theme
2// App Router note: render a synchronous inline <script> directly in <head>
3// (NOT next/script with beforeInteractive — that strategy is not supported in
4// App Router). The script body must be a hard-coded string literal so React's
5// dangerouslySetInnerHTML never sees user-controlled content.
6
7const PREFERENCES_INIT = `(function(){try{var d=document.documentElement;var t=localStorage.getItem('theme');if(t==='light'||t==='dark')d.setAttribute('data-theme',t);var p=localStorage.getItem('palette');if(p==='default'||p==='sunset'||p==='cyberpunk'||p==='forest')d.setAttribute('data-palette',p);var n=localStorage.getItem('density');if(n==='compact'||n==='normal'||n==='comfortable')d.setAttribute('data-density',n)}catch(e){}})();`
8
9export default function RootLayout({ children }) {
10 return (
11 <html
12 lang="en"
13 data-theme="light"
14 data-palette="default"
15 data-density="normal"
16 suppressHydrationWarning
17 >
18 <head>
19 {/* Synchronous, allow-list validated. Must come before any stylesheet
20 link so first paint sees the persisted preferences. */}
21 <script dangerouslySetInnerHTML={{ __html: PREFERENCES_INIT }} />
22 </head>
23 <body suppressHydrationWarning>{children}</body>
24 </html>
25 )
26}
27
28// Pair this with usePersistedAttr from damo-ui — its lazy-init useState makes
29// React's first commit value match the script's DOM write, so the post-paint
30// effect is a no-op instead of an undo.
31
7. Scoped islands
The attributes work on any element, not only <html>. Wrap a section in data-theme="dark" to flip just that subtree.
1// Scope a theme to a section instead of the whole page
2<section data-theme="dark" className="bg-background text-foreground">
3 {/* All children render in dark vars regardless of the page-level theme */}
4 <h2>Dark island inside a light page</h2>
5</section>
6
Tip
The Theme Generator authors light + dark variants visually and exports the CSS overrides for steps 1 and 2. Skip the manual hex hunt.