Skip to main content
Version: 0.14

CSS-in-JS integration

@unpunnyfuns/swatchbook-integrations/css-in-js exposes a typed JS accessor whose leaves are var(--<cssVarPrefix>-*) string references. It's a drop-in for any ThemeProvider that passes its theme through as-is — emotion, styled-components, or a hand-rolled provider. The toolbar flips every value at once via CSS cascade.

Install

npm install -D @unpunnyfuns/swatchbook-integrations

No additional Vite plugins required — the virtual module is plain JavaScript.

Wire

.storybook/main.ts:

import { defineMain } from '@storybook/react-vite/node';
import cssInJsIntegration from '@unpunnyfuns/swatchbook-integrations/css-in-js';

export default defineMain({
addons: [
{
name: '@unpunnyfuns/swatchbook-addon',
options: {
configPath: '../swatchbook.config.ts',
integrations: [cssInJsIntegration()],
},
},
],
});

In any story or component:

import { theme, color, space, radius, shadow } from 'virtual:swatchbook/theme';

What gets generated

Per-group exports plus an aggregate theme constant, one leaf per DTCG path:

export const color = {
"accent": {
"bg": "var(--sb-color-accent-bg)",
"bgHover": "var(--sb-color-accent-bg-hover)",
"fg": "var(--sb-color-accent-fg)"
},
"surface": {
"default": "var(--sb-color-surface-default)",
// …
},
// …
};
export const space = {
"md": "var(--sb-space-md)",
// …
};

export const theme = { color, space, radius, shadow, typography /* … */ };

Values are stable across tuples. The swatchbook toolbar flipping data-sb-mode="Dark" doesn't change any value in this module — it changes what --sb-color-surface-default resolves to at the CSS level. That's the whole point: one theme object, any number of tuples, runtime switching via cascade.

Usage patterns

emotion / styled-components

Wrap the preview in a ThemeProvider. Any <Provider theme={theme}> works because the theme is just a plain object.

// .storybook/preview.tsx
import type { ReactElement } from 'react';
import { ThemeProvider } from '@emotion/react'; // or 'styled-components'
import { theme } from 'virtual:swatchbook/theme';

export default {
decorators: [
(Story: () => ReactElement) => (
<ThemeProvider theme={theme}>
<Story />
</ThemeProvider>
),
],
};

Your styled components interpolate as usual:

const Button = styled.button`
background: ${(props) => props.theme.color.accent.bg};
color: ${(props) => props.theme.color.accent.fg};
padding: ${(props) => props.theme.space.md};
`;

props.theme.color.accent.bg returns "var(--sb-color-accent-bg)"; CSS resolves the var; toolbar flips redefine it.

Direct references (no provider)

You don't need a provider. Named exports work anywhere a CSS value belongs — inline styles, sx props, template literals, plain CSS-in-JS:

import { color, space, radius } from 'virtual:swatchbook/theme';

<div style={{
background: color.surface.raised,
padding: space.lg,
borderRadius: radius.md,
}} />

Tailwind sx-style prop

Tailwind Variants / Stitches / Vanilla Extract: any library that takes theme keys and emits CSS can consume these leaves.

Naming

  • Dashed DTCG segments (color.accent.bg-hover) render as color.accent.bgHover in the accessor. CamelCased for valid JS identifiers without bracket notation.
  • Numeric DTCG segments render bare (color.palette.neutral.500color.palette.neutral[500]). Leading-zero numerics (size.050) stay quoted because bare 050 is an octal under strict mode.
  • Top-level groups map to exports: color, space, radius, shadow, typography, etc.

HMR

Editing token files re-renders the virtual module; the preview picks up the fresh accessor on the next request. React components re-render with the new theme without losing args or scroll state — same channel the tokens virtual module uses.

What this doesn't do

  • Doesn't produce a resolved-value theme. Leaves are var() references, not literal colours. This matters for libraries that derive dependent slots at factory time — MUI's createTheme() computes palette.primary.light / contrastText from the base, which can't happen from a string reference. For MUI, Vuetify configs, or Bootstrap SCSS maps that need literal values per named theme, run Terrazzo's CLI against the same DTCG sources with @terrazzo/plugin-js. Out-of-scope for the display-side integrations.
  • Doesn't handle composite tokens structurally. A DTCG typography composite lives as a single var() reference in the accessor — not as { fontFamily, fontSize, … } sub-fields. If a library needs the sub-fields (MUI's typography slots), same caveat as above.
  • Doesn't auto-inject a ThemeProvider. You wire the provider yourself — the integration has no opinion on which library you pick.