Warning
Tokenami is still in early development. You might find bugs or missing features. Before reporting issues, please check our existing issues first.
- Installation
- Get started
- Introduction
- Core concepts
- Styling
- Design systems
- Advanced usage
- TypeScript integration
- Troubleshooting
- Community
Jump right in with our Vite starter, or configure your own project. Tokenami generates static styles, provides a lightweight ~2.6kb utility for specificity-safe style composition, and includes a TypeScript plugin to enhance the developer experience.
Using Vite
npm install -D tokenami @tokenami/unplugin && npm install @tokenami/cssnpx tokenami init{
"include": [".tokenami/tokenami.env.d.ts", "src"],
"compilerOptions": {
"plugins": [{ "name": "tokenami" }]
}
}- Make sure your editor uses the workspace TypeScript version (see VS Code docs).
- For VS Code–based editors, enable string suggestions.
import * as tokenami from '@tokenami/unplugin';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [tokenami.vite({ output: './src/styles.css' })],
});Vite handles special character escaping for style attributes, so prevent Tokenami from double-escaping, and then use your configured css utility instead of importing from @tokenami/css:
import { createCss } from '@tokenami/css';
import config from '../.tokenami/tokenami.config';
export const css = createCss(config, { escapeSpecialChars: false });Import the configured output stylesheet in your app root:
import './styles.css';Only Vite is supported for now. For other bundlers and frameworks, use the CLI setup instead, or feel free to open a GitHub issue if plugin support for another tool is something you need.
Using CLI
npm install -D tokenami && npm install @tokenami/cssnpx tokenami init{
"include": [".tokenami/tokenami.env.d.ts", "src"],
"compilerOptions": {
"plugins": [{ "name": "tokenami" }]
}
}- Make sure your editor uses the workspace TypeScript version (see VS Code docs).
- For VS Code–based editors, enable string suggestions.
npx tokenami --output ./public/styles.css --watchAdd the generated CSS file to the <head> of your document:
<link rel="stylesheet" href="/styles.css" />import { css, type Variants, type TokenamiStyle } from '@tokenami/css';
type ButtonVariants = Variants<typeof button>;
type ButtonElementProps = TokenamiStyle<React.ComponentProps<'button'>>;
interface ButtonProps extends ButtonElementProps, ButtonVariants {}
function Button({ size = 'default', ...props }: ButtonProps) {
const [cn, sx] = button({ size });
return <button {...props} className={cn(props.className)} style={sx(props.style)} />;
}
const button = css.compose({
'--display': 'flex',
'--align-items': 'center',
'--justify-content': 'center',
'--background-color': 'var(--color_gray5)',
'--color': 'var(--color_gray12)',
'--gap': 0.5,
'--hover_background-color': 'var(--color_gray6)',
'--hover_color': 'var(--color_gray12)',
variants: {
size: {
default: { '--padding-block': 1, '--padding-inline': 3 },
medium: { '--padding-block': 2, '--padding-inline': 5 },
},
},
});Tokenami isn't another design system. It's a toolkit for building your own that bridges the gap between utility-first CSS frameworks like Tailwind, and CSS-in-JS libraries.
Benefits include:
- 🏷️ Familiar CSS syntax — turn any CSS property into a variable (
padding→--padding) - 🎯 Token-first workflow — define and enforce design decisions from one config
- 💡 Smart authoring — autocomplete and type safety built into your editor
- ☮️ Specificity-safe primitives — compose styles and complex selectors without fighting the cascade
- 📦 Streamlined output — one variable-driven rule instead of lots of utility classes
- ⚡ Dynamic by default — pass token values through props, inline styles, and variants
Utility-first solutions (like Tailwind) and CSS-in-JS both eventually run into the same challenge: composition. As reusable styles, variants, overrides, and composition across component boundaries accumulate, ensuring styles compose predictably without cascade or specificity issues becomes increasingly difficult.
Tokenami is a styling toolkit for teams building design systems that need to scale without specificity creep. It provides first-class primitives for component styles, utilities, variants, selectors, and overrides, while keeping styles colocated with the components that use them.
It extracts what can live in the stylesheet, leaves dynamic values inline when needed, and manages the cascade for you. Name complex selectors in your config, then compose styles without having to think about the cascade or specificity again.
Tokenami uses CSS variables because they let inline styles do more than inline styles normally can.
A variable like --padding maps directly to the CSS property you already know, while named selector
prefixes like --hover_color and --md_padding add pseudo-classes and breakpoints. Tokenami can pass those values inline, then let the generated stylesheet handle when they apply.
Those selectors are powered by CSS variable toggles, so selector complexity does not turn into higher specificity.
And you do not have to type the whole var. Start writing bord and Tokenami will autocomplete the property for you.
Tokenami styles follow a small naming pattern:
- Add
--to any CSS property (padding→--padding) - Prefix selectors with underscores (
--hover_padding) - Prefix breakpoints the same way (
--md_padding) - Combine them when needed (
--md_hover_padding) - Use
---for custom CSS variables
Tip
Want to skip theme setup? Use our official design system which comes with dark mode, fluid typography, RTL support, and more.
Tokenami relies on your theme to provide design system constraints. Create one in .tokenami/tokenami.config:
export default createConfig({
theme: {
color: {
'slate-100': '#f1f5f9',
'slate-700': '#334155',
'sky-500': '#0ea5e9',
},
radii: {
rounded: '10px',
circle: '9999px',
none: 'none',
},
},
});Name your theme groups and tokens however you like. These names become part of your CSS variables.
Use the modes key to define multiple themes. Choose any names for your modes. Tokens that are shared across themes should be placed in a root object:
export default createConfig({
theme: {
modes: {
light: {
color: {
primary: '#f1f5f9',
secondary: '#334155',
},
},
dark: {
color: {
primary: '#0ea5e9',
secondary: '#f1f5f9',
},
},
},
root: {
radii: {
rounded: '10px',
circle: '9999px',
none: 'none',
},
},
},
});This creates .theme-light and .theme-dark classes. Add them to your page to switch themes.
Customise the theme selector using the themeSelector config:
export default createConfig({
themeSelector: (mode) => (mode === 'root' ? ':root' : `[data-theme=${mode}]`),
});Tokenami uses a grid system for spacing. When you pass a number to properties like padding and margin, it multiplies that number by your grid value. For example, with a grid of 4px, using --padding: 2 adds 8px of padding.
By default, the grid is set to 0.25rem. You can change it in your config:
export default createConfig({
grid: '10px',
// ... rest of your config
});Use arbitrary selectors for one-off callsite overrides:
<Card
style={{
'--{&:hover}_color': 'var(--color_primary)',
'--{&:has(:focus)}_border-color': 'var(--color_highlight)',
'--{&[data-state=open]}_border-color': 'var(--color_primary)',
// use underscore for spaces in your selector
'--{&_p}_color': 'var(--color_primary)',
}}
/>They can be used to style the current element, and its descendants only.
Arbitrary selectors are intended for one-off local selectors. They have lower priority than named selectors. If ordering matters, promote the selector to your config.
You can use one-off inline CSS values anywhere you need to step outside your theme.
<div style={css({ '--padding': '20px' })} />If you want TypeScript to prevent arbitrary values for properties configured in your design system, enable strict mode:
export default createConfig({
strict: true,
// ...
});Keep component defaults, interaction states, and variants together in css.compose, so base styles can be extracted into the stylesheet. Use css for one-off styles on native elements and reusable style utilities.
css.compose returns a function that provides a class name helper for the extracted styles and a style helper for resolving selected variants and prop overrides.
Here's a basic example:
const button = css.compose({
'--padding': 4,
'--background': 'var(--color_primary)',
'--hover_background': 'var(--color_primary-dark)',
});
function Button(props) {
const [cn, sx] = button();
return <button {...props} className={cn(props.className)} style={sx(props.style)} />;
}With no overrides, the output uses the extracted class, for example:
<button class="tk-button">click me</button>The variants object lets you define different style variations or boolean states:
const card = css.compose({
'--border-radius': 'var(--radii_rounded)',
'--color': 'var(--color_white)',
'--font-size': 'var(--text-size_sm)',
variants: {
color: {
blue: { '--background-color': 'var(--color_blue)' },
green: { '--background-color': 'var(--color_green)' },
},
size: {
small: { '--padding': 2 },
large: { '--padding': 6 },
},
emphasised: {
true: {
'--outline': '2px solid',
'--outline-color': 'var(--color_white)',
'--outline-offset': '2px',
},
},
},
});Use multiple variants together:
function Card({ color = 'blue', size = 'small', emphasised = false, ...props }) {
const [cn, sx] = card({ color, size, emphasised });
return <div {...props} className={cn(props.className)} style={sx(props.style)} />;
}For components whose props use TokenamiStyle, pass overrides directly through style:
<Button style={{ '--margin-left': 'auto', '--padding-inline': 6 }}>Save playlist</Button>The component passes props.style to the style function returned by compose, which processes the Tokenami properties and resolves them against the component styles. The caller does not need to wrap them in css. Use variants for recurring choices such as size or appearance, and the style prop for callsite-specific adjustments.
Use includes to combine component style definitions and reusable css utilities:
// Reusable focus utility
const focusable = css({
'--focus_outline': 'var(--line_ring)',
'--outline-offset': 'var(--offset_ring)',
});
const button = css.compose({
'--background': 'var(--color_iris9)',
'--color': 'var(--color_white)',
'--padding': 4,
});
const tomatoButton = css.compose({
includes: [button, focusable],
'--background': 'tomato',
'--font-weight': 'bold',
});
function TomatoButton(props) {
const [cn, sx] = tomatoButton();
return <button {...props} className={cn(props.className)} style={sx(props.style)} />;
}Included component definitions contribute their extracted classes. The non-conflicting font-weight stays in the stylesheet. The focus utility and conflicting background appear inline so the extending definition takes precedence. For example:
<button
class="tk-button tk-tomato-button"
style="--focus_outline: var(--line_ring); --outline-offset: var(--offset_ring); --background: tomato;"
>
click me
</button>Use css for one-off styles on native elements, such as layout, spacing, or appearance specific to a callsite.
<div
style={css({
'--display': 'flex',
'--align-items': 'center',
'--justify-content': 'flex-end',
'--gap': 2,
'--padding': 4,
})}
>
<Button style={{ '--background': 'red' }}>Cancel</Button>
<Button>Save</Button>
</div>For components whose props use TokenamiStyle, pass overrides directly through style. See component overrides.
Configure css and css.compose using createCss when your Tokenami config changes how properties are parsed, e.g. property aliases.
// css.ts
import { createCss } from '@tokenami/css';
import config from '../.tokenami/tokenami.config';
export const css = createCss(config);This ensures runtime styles are transformed the same way as your generated stylesheet.
escapeSpecialChars defaults to true. With Vite, disable it to avoid double-escaping property names, which prevents inline styles from matching the generated stylesheet:
export const css = createCss(config, { escapeSpecialChars: false });Tokenami eases the friction of creating portable design systems, whether you're building your own or using our official one.
Our official design system comes with:
- Global reset based on Preflight from Tailwind
- Radix UI colours with automatic dark mode
- Fluid spacing and font sizes for responsive design
- Right-to-left support built in (
padding-leftbecomespadding-inline-startetc.) - Short aliases for common properties (e.g.
--pfor padding)
Follow the @tokenami/ds docs to get started.
Create a shared Tokenami config + stylesheet package, and publish it for projects to consume. If the consuming project also uses Tokenami, they should include your design system in their config:
import designSystemConfig from '@acme/design-system';
import { createConfig } from '@tokenami/css';
export default createConfig({
...designSystemConfig,
include: ['./app/**/*.{ts,tsx}', 'node_modules/@acme/design-system/tokenami.css'],
});Projects that consume a Tokenami design system do not need to be using Tokenami themselves though. If they're not using Tokenami, they can reference their stylesheet after the design system stylesheet and their styles will override accordingly.
Provide global styles in your config to include them as part of your design system.
export default createConfig({
globalStyles: {
'*, *::before, *::after': {
boxSizing: 'border-box',
},
body: {
fontFamily: 'system-ui, sans-serif',
lineHeight: 1.5,
margin: 0,
padding: 0,
},
},
});Define your breakpoints in the responsive config:
export default createConfig({
responsive: {
md: '@media (min-width: 700px)',
lg: '@media (min-width: 1024px)',
'md-self': '@container (min-width: 400px)', // Container queries work too!
},
});Use them by adding the breakpoint name before any property:
<div
style={css({
'--padding': 2, // Base padding
'--md_padding': 4, // Padding at medium breakpoint
'--lg_padding': 6, // Padding at large breakpoint
'--md-self_padding': 8, // Padding when container is medium
})}
/>Add keyframes to your config and reference them in your theme:
export default createConfig({
keyframes: {
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' },
},
},
theme: {
anim: {
wiggle: 'wiggle 1s ease-in-out infinite',
},
},
});Use the animation:
const notification = css.compose({
'--animation': 'var(--anim_wiggle)',
});Tokenami has some advanced features that can help you build more powerful design systems.
Named selectors let you give complex selectors simple names and controls their override order.
Some common selectors
are included, but you can configure your own. Use the ampersand (&) to mark where the current
element's selector should be injected:
export default createConfig({
selectors: {
'parent-hover': '.parent:hover > &',
// Nested selectors work too
hover: ['@media (hover: hover) and (pointer: fine)', '&:hover'],
},
});Using the named selectors:
const previewButton = css.compose({
'--parent-hover_color': 'var(--color_primary)',
});
function PreviewButton(props) {
const [cn, sx] = previewButton();
return <button {...props} className={cn(props.className)} style={sx(props.style)} />;
}
<div className="parent">
<img src="..." alt="" />
<PreviewButton>Preview</PreviewButton>
</div>;Tokenami selectors are CSS variable toggles,
so properties like --hover_color have a --hover toggle that activates when your selector
matches. Because of that, selector complexity does not increase specificity. You can make named
selectors as elaborate as you need without fighting the cascade.
For example, the official design system uses a hover selector that only applies with fine pointers and skips disabled elements:
hover: ['@media (hover: hover) and (pointer: fine)', '&:not(:disabled):hover'],That level of guard-railing would often push you toward higher-specificity CSS in other systems and make things harder for teams to maintain over time. In Tokenami it does not.
What matters when multiple selectors match at once is the order you define selectors in your config. Selectors defined later override those that come before.
export default createConfig({
selectors: {
hover: '&:not(:disabled):hover',
focus: '&:focus',
},
});const button = css.compose({
'--color': 'var(--color_neutral-700)',
'--focus_color': 'var(--color_primary)',
'--hover_color': 'var(--color_neutral-800)',
});With focus listed after hover in config, --focus_color overrides --hover_color when the element is both hovered and focused.
Aliases allow you to create shorthand names for properties. Configure aliases in your Tokenami config:
export default createConfig({
aliases: {
p: ['padding'],
px: ['padding-inline'],
py: ['padding-block'],
size: ['width', 'height'],
},
});Aliases change how Tokenami properties map to CSS properties, so use createCss to configure the runtime utility with your generated config. This keeps runtime styles aligned with the generated stylesheet and enables alias composition across component boundaries.
Use aliases the same way as any other Tokenami property:
<div style={css({ '--p': 4, '--px': 2, '--size': '100%' })} />Tokenami maps your properties to some default theme keys out of the box. For example, --border-color accepts tokens from your color theme object, while --padding works with your grid system. You can customise these mappings in the properties key:
export default createConfig({
grid: '0.25rem',
theme: {
container: {
half: '50%',
full: '100%',
},
pet: {
cat: '"🐱"',
dog: '"🐶"',
},
},
properties: {
padding: ['grid'],
width: ['grid', 'container'],
height: ['grid', 'container'],
content: ['pet'],
},
});With this configuration, passing --padding: 4 would use the grid mapping to output 1rem of padding. Passing var(--container_half) to a content property would error because container does not exist in its property config, but var(--pet_dog) would be allowed:
<div
style={css({
'--width': 'var(--container_half)', // Works ✅
'--content': 'var(--pet_cat)', // Works ✅
'--content': 'var(--container_half)', // Error ❌
})}
/>Create your own properties for design system features. For example, make gradient properties that use your colour tokens by adding them to the customProperties key:
export default createConfig({
theme: {
color: {
primary: '#f1f5f9',
secondary: '#334155',
},
gradient: {
// use your custom properties to configure the gradient
radial: 'radial-gradient(circle at top, var(--gradient-from), var(--gradient-to) 80%)',
},
},
properties: {
'background-image': ['gradient'],
},
customProperties: {
'gradient-from': ['color'],
'gradient-to': ['color'],
},
});Then use the gradient properties:
const banner = css.compose({
'--background-image': 'var(--gradient_radial)',
'--gradient-from': 'var(--color_primary)',
'--gradient-to': 'var(--color_secondary)',
});
function Banner(props) {
const [cn, sx] = banner();
return <div {...props} className={cn(props.className)} style={sx(props.style)} />;
}Override a gradient color at a particular callsite:
<Banner style={{ '--gradient-from': 'var(--color_secondary)' }} />Use the Variants type to extend your component props with the available variants:
import { type Variants } from '@tokenami/css';
type ButtonElementProps = React.ComponentPropsWithoutRef<'button'>;
interface ButtonProps extends ButtonElementProps, Variants<typeof button> {}Components defined with css.compose can use TokenamiStyle to accept overrides as Tokenami properties on their style prop.
import { type TokenamiStyle, css } from '@tokenami/css';
interface ButtonProps extends TokenamiStyle<React.ComponentProps<'button'>> {}
const button = css.compose({ '--padding': 4 });
function Button(props: ButtonProps) {
const [cn, sx] = button();
return <button {...props} className={cn(props.className)} style={sx(props.style)} />;
}Now you can pass Tokenami properties with type checking:
<Button style={{ '--padding': 4 }} />Tokenami uses widened types during development for better performance. When you run tsc in the command line, it uses these widened types and won't show Tokenami type errors.
For accurate type checking in CI, run both commands:
tokenami check; tsc --noEmitCommon questions and how to solve them. If you need additional support or encounter any issues, Need help? Join the Discord server.
VS Code won't suggest completions for partial strings by default. This prevents Tokenami from updating its suggestions. To fix, add the following to .vscode/settings.json:
{
"editor.quickSuggestions": {
"strings": true
}
}| BEFORE | AFTER |
|---|---|
![]() |
![]() |
Tokenami currently works with:
React |
Preact |
Vue |
SolidJS |
Qwik |
|---|---|---|---|---|
| ✅ | ✅ | ✅ | ✅ | ✅ |
We're still in early development and plan to support more libraries in the future.
Tokenami relies on cascade layers, so it works in browsers that support @layer:
![]() Edge |
![]() Firefox |
![]() Chrome |
![]() Safari |
![]() iOS Safari |
![]() Opera |
|---|---|---|---|---|---|
| 99+ | 97+ | 99+ | 15.4+ | 15.4+ | 86+ |
Tokenami is officially supported in the following editors:
You can use browserslist to add autoprefixing to your CSS properties in the generated CSS file. However, Tokenami currently doesn't support vendor-prefixed values, which is being tracked in this issue.
Important
Tokenami does not support browsers below the listed supported browser versions. We recommend using "browserslist": ["supports css-cascade-layers"] if you're unsure.
Before raising a bug, please check if it's already in our todo list. Need help? Join our Discord server.
- Paweł Błaszczyk (@pawelblaszczyk_)
A big thanks to:
- Tailwind V3 for inspiring many of Tokenami's features
- Stitches for variants inspiration
- CSS Hooks for custom selectors inspiration
- Lightning CSS for generating the Tokenami stylesheet
Please do check out these projects if Tokenami isn't quite what you're looking for.







