Skip to content

tokenami/tokenami

Repository files navigation

image

CSS-in-JS Reinvented for Portable, Type-Safe Design Systems

A modern approach to just-in-time CSS using atomic CSS variables—no bundler required.

React support Preact support Vue support SolidJS support Qwik support

Warning

Tokenami is still in early development. You might find bugs or missing features. Before reporting issues, please check our existing issues first.

Contents

Installation

Jump right in with our Vite starter, or configure your own project. Tokenami generates static styles, provides a lightweight ~2.5kb utility for specificity-safe style composition, and includes a TypeScript plugin to enhance the developer experience.

Using Vite

1. Install packages

npm install -D tokenami @tokenami/unplugin && npm install @tokenami/css

2. Initialise your project

npx tokenami init

3. Configure TypeScript (tsconfig.json or jsconfig.json)

{
  "include": [".tokenami/tokenami.env.d.ts", "src"],
  "compilerOptions": {
    "plugins": [{ "name": "tokenami" }]
  }
}

4. Add the Vite plugin

import * as tokenami from '@tokenami/unplugin';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [tokenami.vite({ output: 'styles.css' })],
});

5. Import the generated stylesheet

Import the configured output path 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

1. Install packages

npm install -D tokenami && npm install @tokenami/css

2. Initialise your project

npx tokenami init

3. Configure TypeScript (tsconfig.json or jsconfig.json)

{
  "include": [".tokenami/tokenami.env.d.ts", "src"],
  "compilerOptions": {
    "plugins": [{ "name": "tokenami" }]
  }
}

4. Start the Tokenami CLI build process

npx tokenami --output ./public/styles.css --watch

5. Reference the generated CSS file

Add the generated CSS file to the <head> of your document:

<link rel="stylesheet" href="/styles.css" />

Get started

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

Introduction

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

Why Tokenami?

Utility-first CSS is fast and themed, but reusable component styles often end up as long class strings or move into @apply, away from the component you are working on. CSS-in-JS keeps styles colocated, but often leaves design-system structure up to you. In both cases, composition and arbitrary selectors can quickly turn into cascade or specificity problems.

Tokenami is a styling toolkit for teams building design systems that need to scale without specificity creep. It keeps the colocated workflow while providing first-class primitives for component styles, utilities, variants, selectors, and overrides.

It extracts what can live in the stylesheet, leaves dynamic values inline when needed, and manages the cascade for you. Define selector order once in your config, then compose styles and write complex selectors without having to worry about the cascade again.

Why CSS variables?

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 prefixes like --hover_color or --md_padding add pseudo-classes and breakpoints. Tokenami can pass those values inline, then let the generated stylesheet decide 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.

Core concepts

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

Theming

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.

Multiple themes

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}]`),
});

Grid values

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
});

Arbitrary selectors

Use arbitrary selectors to prototype quickly:

<div
  style={css({
    '--{&: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 values

You can avoid TypeScript errors for one-off inline values by using a triple-dash fallback.

<div style={css({ '--padding': 'var(---, 20px)' })} />

This prevents TypeScript errors and sets padding to 20px. Tokenami intentionally adds friction to the developer experience here. This is to encourage sticking to your theme guidelines and to help you quickly spot values in your code that don't.

Styling

CSS utility

The css utility is used to author your styles and helps with overrides and avoiding specificity issues. Use css for inline styles.

Usage

Pass your base styles as the first parameter, then any overrides:

function Button(props) {
  return (
    <button
      {...props}
      style={css(
        { '--padding': 4 }, // Base styles
        props.style // Overrides
      )}
    />
  );
}

Overrides

Add conditional styles as extra parameters. The last override wins:

function Button(props) {
  const disabled = props.disabled && {
    '--opacity': 0.5,
    '--pointer-events': 'none',
  };

  return (
    <button
      {...props}
      style={css(
        { '--padding': 4 }, // Base styles
        disabled, // Conditional styles
        props.style // Props override
      )}
    />
  );
}

Composing components

The css.compose API helps you build reusable components with variants. Styles in the compose block are extracted into your stylesheet and replaced with a class name to reduce repetition in your markup.

Here's a basic example:

const button = css.compose({
  '--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)} />;
}

Output:

<button class="tk-abc">click me</button>

Variants

The variants object lets you define different style variations:

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

Use multiple variants together:

function Card(props) {
  const [cn, sx] = card({ color: 'blue', size: 'large' });
  return <div {...props} className={cn(props.className)} style={sx(props.style)} />;
}

Variants are treated like overrides, so appear inline:

<div class="tk-abc" style="--background-color: var(--color_blue); --padding: 6;">boop</div>

Extending styles

Use includes to combine styles from multiple components or css utilities.

// Reusable focus styles (will appear inline)
const focusable = css({
  '--focus_outline': 'var(--outline_sm)',
  '--outline-offset': 'var(--outline-offset_sm)',
});

// Base button styles (composed so will be extracted into stylesheet)
const button = css.compose({
  '--background': 'var(--color_primary)',
  '--color': 'var(--color_white)',
  '--padding': 4,
});

// New button that includes both
const tomatoButton = css.compose({
  includes: [button, focusable],
  '--background': 'var(--color_tomato)',
});

Conflicting styles (e.g. --background) are moved inline to override:

<button
  class="tk-abc"
  style="--focus_outline: var(--outline_sm); --outline-offset: var(--outline-offset_sm); --background: var(--color_tomato);"
>
  click me
</button>

Design systems

Tokenami eases the friction of creating portable design systems, whether you're building your own or using our official one.

Using the official system

Our official design system comes with:

Follow the @tokenami/ds docs to get started.

Building your own system

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.

Global styles

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

Breakpoints

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
  })}
/>

Animation

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

Apply the animation to an element:

<div style={css({ '--animation': 'var(--anim_wiggle)' })} />

Advanced usage

Tokenami has some advanced features that can help you build more powerful design systems.

Custom selectors

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

Use them in your components:

<div className="parent">
  <img src="..." alt="" />
  <button style={css({ '--parent-hover_color': 'var(--color_primary)' })} />
</div>

Selector specificity

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 selectors as elaborate as you need without fighting the cascade.

For example, the official design system uses a hover selector that only applies on 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: '&:hover',
    focus: '&:focus', // wins over hover when both match
  },
});
<button
  style={css({
    '--color': 'var(--color_neutral-700)',
    '--hover_color': 'var(--color_neutral-800)',
    '--focus_color': 'var(--color_primary)',
  })}
/>

With hover listed before focus, --focus_color overrides --hover_color when the element is both hovered and focused.

Property aliases

Aliases allow you to create shorthand names for properties. When using custom aliases, the css utility must be configured to ensure conflicts are resolved correctly across component boundaries.

1. Create a file in your project to configure the utility. You can name this file however you like:

// css.ts
import { createCss } from '@tokenami/css';
import config from '../.tokenami/tokenami.config';

export const css = createCss(config);

2. Configure the aliases in your config file:

export default createConfig({
  aliases: {
    p: ['padding'],
    px: ['padding-inline'],
    py: ['padding-block'],
    size: ['width', 'height'],
  },
});

3. Use the aliases:

<div style={css({ '--p': 4, '--px': 2, '--size': '100%' })} />

Theming properties

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({
  theme: {
    container: {
      half: '50%',
      full: '100%',
    },
    pet: {
      cat: '"🐱"',
      dog: '"🐶"',
    },
  },
  properties: {
    width: ['grid', 'container'],
    height: ['grid', 'container'],
    content: ['pet'],
  },
});

With this configuration, 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 ❌
  })}
/>

Custom properties

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 them as follows:

<div
  style={css({
    '--background-image': 'var(--gradient_radial)',
    '--gradient-from': 'var(--color_primary)',
    '--gradient-to': 'var(--color_secondary)',
  })}
/>

TypeScript integration

Utility types

Variants

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> {}

TokenamiStyle

Components styled with the css utility can use TokenamiStyle to type their style prop if you want it to accept Tokenami properties.

import { type TokenamiStyle, css } from '@tokenami/css';

interface ButtonProps extends TokenamiStyle<React.ComponentProps<'button'>> {}

function Button(props: ButtonProps) {
  return <button {...props} style={css({}, props.style)} />;
}

Now you can pass Tokenami properties with type checking:

<Button style={{ '--padding': 4 }} />

TokenValue

Use TokenValue to get a union of CSS variable tokens based on your theme.

Given this theme:

export default createConfig({
  theme: {
    color: {
      'slate-100': '#f1f5f9',
      'slate-700': '#334155',
    },
    radii: {
      rounded: '10px',
      circle: '9999px',
    },
  },
});

It will output the following types:

import { type TokenValue } from '@tokenami/css';

type Color = TokenValue<'color'>; // var(--color_slate-100) | var(--color_slate-700)
type Radii = TokenValue<'radii'>; // var(--radii_rounded) | var(--radii_circle)

CI setup

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 --noEmit

Troubleshooting

Common questions and how to solve them. If you need additional support or encounter any issues, Need help? Join the Discord server.

Enable string completions

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
CleanShot 2024-09-08 at 14 10 10 CleanShot 2024-09-08 at 14 09 43

Supported libraries

Tokenami currently works with:


React

Preact

Vue

SolidJS

Qwik

We're still in early development and plan to support more libraries in the future.

Supported browsers

Tokenami relies on cascade layers, so it works in browsers that support @layer:

Edge
Edge
Firefox
Firefox
Chrome
Chrome
Safari
Safari
iOS Safari
iOS Safari
Opera
Opera
99+ 97+ 99+ 15.4+ 15.4+ 86+

Supported editors

Tokenami is officially supported in the following editors:

Browserslist

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.

Community

Contributing

Before raising a bug, please check if it's already in our todo list. Need help? Join our Discord server.

Contributors

Credits

A big thanks to:

Please do check out these projects if Tokenami isn't quite what you're looking for.

About

The utility-first CSS lib for type-safe design systems. No runtime injection, no class soup, no specificity wars—just predictable styling with atomic CSS variables.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors