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 ascolor.accent.bgHoverin the accessor. CamelCased for valid JS identifiers without bracket notation. - Numeric DTCG segments render bare (
color.palette.neutral.500→color.palette.neutral[500]). Leading-zero numerics (size.050) stay quoted because bare050is 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'screateTheme()computespalette.primary.light/contrastTextfrom 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, you'd need a different emission story — seeemitViaTerrazzowithselection: 'presets'and@terrazzo/plugin-js. Not currently shipped as an integration. - Doesn't handle composite tokens structurally. A DTCG
typographycomposite lives as a singlevar()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.