Multi-Theme Tokens
Bamboo supports advance token definition beyond just light/dark mode; theming beyond just dark mode. You can define multi-theme tokens using nested conditions.
Let's say your application supports a pink and blue theme, and each theme can have a light and dark mode. Let's see how to model this in Bamboo.
We'll start by defining the following conditions for these theme and color modes:
bamboo.config.ts
const config = {
conditions: {
light: '[data-color-mode=light] &',
dark: '[data-color-mode=dark] &',
pinkTheme: '[data-theme=pink] &',
blueTheme: '[data-theme=blue] &',
},
}Conditions are a way to provide preset css selectors or media queries for use in your Bamboo project
Next, we'll define a colors.text semantic token for the pink and blue theme.
bamboo.config.ts
const theme = {
// ...
semanticTokens: {
colors: {
text: {
value: {
_pinkTheme: 'token(colors.pink.500)',
_blueTheme: 'token(colors.blue.500)',
},
},
},
},
}Next, we'll modify colors.text to support light and dark color modes for each theme.
bamboo.config.ts
const theme = {
// ...
semanticTokens: {
colors: {
text: {
value: {
_pinkTheme: { base: 'token(colors.pink.500)', _dark: 'token(colors.pink.300)' },
_blueTheme: { base: 'token(colors.blue.500)', _dark: 'token(colors.blue.300)' },
},
},
},
},
}Now, you can use the text token in your styles, and it will automatically change based on the theme and the color
scheme.
// use pink and dark mode theme
<html data-theme="pink" data-color-mode="dark">
<body>
<h1 className={css({ color: 'text' })}>Hello World</h1>
</body>
</html>
// use pink and light mode theme
<html data-theme="pink">
<body>
<h1 className={css({ color: 'text' })}>Hello World</h1>
</body>
</html>
Multi-Themes
The above example shows you how to define multi-theme tokens using nested conditions but you can also define clearly
separated themes using theme.variants in the config.
This allows you to apply a theme on multiple tokens at once, using data attributes and CSS variables.
Theme variants can be applied using the data-bamboo-theme attribute with the theme key as the value.
bamboo.config.ts
import { defineConfig } from '@bamboocss/dev'
export default defineConfig({
// ...
// main theme
theme: {
extend: {
tokens: {
colors: {
text: { value: 'green' },
},
},
semanticTokens: {
colors: {
body: {
value: {
base: 'token(colors.green.600)',
_osDark: 'token(colors.green.400)',
},
},
},
},
},
variants: {
primary: {
tokens: {
colors: {
text: { value: 'red' },
},
},
semanticTokens: {
colors: {
muted: { value: 'token(colors.red.200)' },
body: {
value: {
base: 'token(colors.red.600)',
_osDark: 'token(colors.red.400)',
},
},
},
},
},
secondary: {
tokens: {
colors: {
text: { value: 'blue' },
},
},
semanticTokens: {
colors: {
muted: { value: 'token(colors.blue.200)' },
body: {
value: {
base: 'token(colors.blue.600)',
_osDark: 'token(colors.blue.400)',
},
},
},
},
},
},
},
})Pregenerating themes
By default, no additional theme variant is generated, you need to specify the specific themes you want to generate in
staticCss.themes to include them in the CSS output.
bamboo.config.ts
import { defineConfig } from '@bamboocss/dev'
export default defineConfig({
// ...
staticCss: {
themes: ['primary', 'secondary'],
},
})An excerpt of the generated CSS, showing the base tokens and the primary variant:
@layer tokens {
:where(:root, :host) {
color-scheme: light dark;
--colors-text: green;
--colors-body: light-dark(var(--colors-green-600), var(--colors-green-400));
}
[data-bamboo-theme='primary'] {
--colors-text: red;
--colors-muted: var(--colors-red-200);
--colors-body: var(--colors-red-600);
}
@media (prefers-color-scheme: dark) {
[data-bamboo-theme='primary'] {
--colors-body: var(--colors-red-400);
}
}
}
The root's light and dark values collapse into one
light-dark() declaration. A theme's do not â its dark
value sits under the theme condition rather than at the top level, so it keeps a media block of its own.
Dynamically importing themes
An alternative way of applying a theme is by using the new styled-system/themes entrypoint where you can import the
themes expected CSS and apply them in your app.
âšī¸ The styled-system/themes will always contain every themes (tree-shaken if not used), whereas staticCss.themes
only applies to the CSS output.
Each theme has a corresponding JSON file with a similar structure:
{
"name": "primary",
"id": "bamboo-theme-primary",
"css": "[data-bamboo-theme=primary] { ... }"
}
Dynamically import a theme using its name
import { getTheme } from '../styled-system/themes'
const theme = await getTheme('primary')
// ^? {
// name: "primary";
// id: string;
// css: string;
// }
Inject the theme styles into the DOM:
import { getTheme, injectTheme } from '../styled-system/themes'
const theme = await getTheme('primary')
injectTheme(document.documentElement, theme) // this returns the injected style element
SSR example with React Router
In React Router framework mode with SSR enabled, load the selected theme in the root
loader and include its CSS in the initial document. This example assumes the primary and secondary variants above
and a root-level styled-system directory:
app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useLoaderData, type LoaderFunctionArgs } from 'react-router'
import { getTheme } from '../styled-system/themes'
import 'virtual:bamboo.css'
export async function loader({ request }: LoaderFunctionArgs) {
const cookie = request.headers.get('Cookie') ?? ''
const savedTheme = cookie
.split(';')
.map((part) => part.trim())
.find((part) => part.startsWith('theme='))
?.slice(6)
const themeName = savedTheme === 'secondary' ? 'secondary' : 'primary'
return { theme: await getTheme(themeName) }
}
export default function App() {
const { theme } = useLoaderData<typeof loader>()
return (
<html lang="en" data-bamboo-theme={theme.name}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<Meta />
<Links />
<style id={theme.id} dangerouslySetInnerHTML={{ __html: theme.css }} />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
</body>
</html>
)
}Only configured theme names reach getTheme; a missing or unrecognized cookie selects primary. The CSS comes from
Bamboo's generated theme files. For client-side switching, use getTheme and injectTheme as above, and save the
selection in the same cookie if it should survive a reload:
import { getTheme, injectTheme, type ThemeName } from '../styled-system/themes'
export async function selectTheme(name: ThemeName) {
const theme = await getTheme(name)
injectTheme(document.documentElement, theme)
document.cookie = `theme=${encodeURIComponent(name)}; Path=/; SameSite=Lax`
}
Theme contract
Finally, you can create a theme contract to ensure that all themes have the same structure:
import { defineThemeContract } from '@bamboocss/dev'
const defineTheme = defineThemeContract({
tokens: {
colors: {
red: { value: '' }, // theme implementations must have a red color
},
},
})
defineTheme({
tokens: {
colors: {
// ^^^^ â Property 'red' is missing in type '{}' but required in type '{ red: { value: string; }; }'
//
// â
fixed with
// red: { value: 'red' },
},
},
})