guides
multiple themes

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' },
    },
  },
})