Just-in-time atomic CSS, in the style attribute.
Fully typed static styles with theming, variant support, and no bundler integration.
Warning
This is a pre-alpha version of tokenami so there will be bugs, breaking changes, and missing features. Please check the existing issues for planned features/known bugs before creating new ones.
If you prefer to get stuck straight in, give Tokenami a try on StackBlitz.
For an enhanced dev experience press CMD+Shift+P
and choose the workspace version of TypeScript. StackBlitz can't match the experience of a local dev environment (no colour swatches in intellisense) but it's a great way to get started.
tokenami-demo.mp4
- Why another CSS library?
- Getting started
- Core concepts
- CSS utility
- CSS compose
- TypeScript
- Advanced
- Support
- Contributors
- Credits
CSS-in-JS solutions that rely on style injection were no longer recommended by the React team, and instead they suggested:
Our preferred solution is to use
<link rel="stylesheet">
for statically extracted styles and plain inline styles for dynamic values. E.g.<div style={{...}}>
In other words—write CSS like we used to. But what about the benefits that CSS-in-JS gave us?
There are CSS-in-JS solutions that extract static rules from your template files into external .css
files, however, these approaches often require bundler integration and come with build-time limitations.
The learning curve can be intimidating but developers invest regardless so they can have type errors and intellisense for their design system tokens as well as style deduping, critical path CSS, scoping, and composition.
Read more
Tailwind CSS adopts a different strategy to achieve these goals:
- We can style inline to prototype quickly
- Editor extensions for intellisense based on your theme
- Statically generated styles with a simple CLI script, no bundler integration
- Atomic CSS so styles have a cap on how large they can grow
On the flip side:
- Removing values from your theme won't flag redundant references
- We must memorise Tailwind's custom class names which spawns things like the Tailwind Cheatsheet
- Specificity issues when composing unless we use third-party packages like tailwind-merge
- Styling inline can be unpleasant to maintain, resulting in third-party packages like cva
- Classes must exist as complete unbroken strings
- Debugging in dev tools is tricky because styles are spread across atomic classes
Tokenami aims to improve some of these areas by using CSS variables instead of CSS properties in the style
attribute, and bringing all necessary tools under one roof. It features:
- Simple naming convention—use the CSS properties you already know, prefixed with double-dash
- Smaller stylesheet made possible by atomic CSS variables
- Config file for defining your theme
- Feature-rich intellisense when authoring styles
- A tiny
css
utility with variants, and responsive variants support - Seamless composition across component boundaries using the
css
utility - Runtime style support e.g.
style={css({ '--color': props.color })}
- Aliasable properties e.g.
style={css({ '--p': 4 })}
for padding - Custom selector support enabling descendant selectors
- Improved debugging experience in dev tools
- Statically generated styles
- No bundler integration
Tokenami offers a CLI tool for generating static styles, a ~3kb CSS utility for authoring your styles, and a TypeScript plugin to enhance the developer experience.
Install using your package manager of choice. For example:
npm install @tokenami/dev @tokenami/ts-plugin -D
npm install @tokenami/css
And then initialise your tokenami project:
npx tokenami init
Add Tokenami to include
and plugins
in your tsconfig.json
or jsconfig.json
.
{
"include": [".tokenami/tokenami.env.d.ts"],
"compilerOptions": {
"plugins": [{ "name": "@tokenami/ts-plugin" }]
}
}
Make sure your editor is configured to use the project's version of TypeScript. You can find instructions for various editors in their documentation, such as for VSCode here.
Run the CLI tool to scan your template files for tokenami properties and build your CSS. This would usually exist as a script in your package.json
.
npx tokenami --output ./public/styles.css --watch
Make sure to adjust the output path to your desired location for styles. It will use ./public/tokenami.css
by default if omitted.
Reference your output CSS file in the <head>
of your document, and start styling inline with Tokenami properties:
import { css } from '@tokenami/css';
function Page() {
return <h1 style={css({ '--margin-top': 0, '--margin-bottom': 5 })}>Hello, World!</h1>;
}
Tokenami relies on your theme to provide design system constraints. There isn't a predefined theme so you must add your own to the .tokenami/tokenami.config
. For example:
module.exports = createConfig({
// ...
responsive: {
medium: '@media (min-width: 700px)',
large: '@media (min-width: 1024px)',
},
theme: {
color: {
'slate-100': '#f1f5f9',
'slate-700': '#334155',
'sky-500': '#0ea5e9',
},
radii: {
rounded: '10px',
circle: '9999px',
none: 'none',
},
},
});
The keys in your responsive
and theme
objects can be anything you wish. These keys will be used to name your tokens (more on this later).
Use the modes
key to set up multiple themes if preferred. The names of your modes can be anything you like:
module.exports = createConfig({
theme: {
modes: {
light: {
color: {
primary: '#f1f5f9',
secondary: '#334155',
},
},
dark: {
color: {
primary: '#0ea5e9',
secondary: '#f1f5f9',
},
},
},
},
});
By default this will apply the CSS variables to .theme-${mode}
classes. Add the classes to an element on your page to apply the relevant theme.
To customise the theme selector, update the themeSelector
config.
module.exports = createConfig({
themeSelector: (mode) => (mode === 'root' ? ':root' : `.theme-${mode}`),
});
With your theme set up, there are only a few rules to remember:
- A Tokenami property is any CSS property prefixed with double dash, e.g.
--font-size
(why?). Use---
(triple dash) to add custom CSS variables to an element. - A Tokenami token is any theme key followed by a value identifier, and separated by an underscore. For example, a
color
object in theme with ared-100
entry maps tovar(--color_red-100)
. - Properties can include selectors like media queries, pseudo-classes, and pseudo-elements separated with an underscore. For instance,
--hover_background-color
,--md_hover_background-color
.
Tokenami uses a grid value for spacing. Properties like padding and margin are multiples of this when passed a numeric value. For example, with a grid set to 4px
, using --padding: 2
adds 8px
of padding to your element.
By default, Tokenami sets the grid to 0.25rem
but you can override it:
module.exports = createConfig({
// ...
grid: '10px',
});
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)',
})}
/>
They can be used to style the current element, and its descendants only.
You can avoid TypeScript errors for one-off inline values by using a triple dash fallback. For instance, --padding: var(---, 20px)
prevents 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.
Define responsive rules in the responsive
object in your config. This can include @container
queries:
module.exports = createConfig({
// ...
responsive: {
medium: '@media (min-width: 1024px)',
'medium-self': '@container (min-width: 400px)',
},
});
Use by following the property spec:
<div style={css({ '--medium_padding': 4 })} />
Responsive rules can also be combined with selectors:
<div style={css({ '--medium_hover_padding': 4 })} />
For documentation on responsive variants, refer to the CSS compose section.
Tokenami supports global styles in your tokenami.config
. It can be useful for including them as part of a design system.
module.exports = createConfig({
// ...
globalStyles: {
'*, *::before, *::after': {
boxSizing: 'border-box',
},
body: {
fontFamily: 'system-ui, sans-serif',
lineHeight: 1.5,
margin: 0,
padding: 0,
},
},
});
Add keyframes to your config and reference them in your theme:
module.exports = createConfig({
// ...
keyframes: {
wiggle: {
'0%, 100%': { transform: 'rotate(-3deg)' },
'50%': { transform: 'rotate(3deg)' },
},
},
theme: {
anim: {
wiggle: 'wiggle 1s ease-in-out infinite',
},
},
});
Use by following the token spec:
<div style={css({ '--animation': 'var(--anim_wiggle)' })} />
Tokenami provides a CSS utility to author your styles and correctly merge them across component boundaries.
The css
utility accepts your base styles as the first parameter, and then any number of overrides as additional parameters.
function Button({ size, style, ...props }) {
return <button {...props} style={css({ '--padding': 4 }, props.style)} />;
}
In the above example we're passing props.style
as an override to ensure composed styles will merge correctly across component boundaries.
Overrides can be applied conditionally and last override wins. They are applied as additional parameters to the css
utility.
function Button({ style, ...props }) {
const disabled = props.disabled && { '--opacity': 0.5 };
return <button {...props} style={css({ '--p': 4 }, disabled, style)} />;
}
Use the css.compose
API to author variants.
const button = css.compose({
'--padding': 4,
variants: {
size: {
small: { '--padding': 2 },
large: { '--padding': 6 },
},
},
});
function Button({ size, style, ...props }) {
return <button {...props} style={button({ size }, style)} />;
}
The function returned by css.compose
accepts your chosen variants as the first parameter, and then any number of overrides as additional parameters.
Responsive variants allow you to prefix the variant name with a responsive key from your configuration. For example, the following button will apply the large size
variant at the medium breakpoint:
function Button() {
return <button style={button({ medium_size: 'large' })} />;
}
To enable it, rename the variants
property to responsiveVariants
. This will generate the atomic CSS for the responsive variants regardless of whether they're used, so it is purposefully opt-in to allow greater control.
const button = css.compose({
'--padding': 4,
- variants: {
+ responsiveVariants: {
size: {
small: { '--padding': 2 },
large: { '--padding': 6 },
},
},
});
Use the Variants
type to extend your component prop types:
import { type Variants } from '@tokenami/css';
type ButtonElementProps = React.ComponentPropsWithoutRef<'button'>;
interface ButtonProps extends ButtonElementProps, Variants<typeof button> {}
Use TokenamiStyle
to accept the css
utility as a value for the style
prop. This prevents errors when props.style
is used for overrides.
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)} />;
}
To improve performance during development, Tokenami widens its types and uses the TypeScript plugin for completions. Using tsc
in the command line defaults to these widened types so it will not highlight errors for your properties or tokens. To get accurate types for CI, do the following:
-
Create a file named
tsconfig.ci.json
orjsconfig.ci.json
. It should extend your original config and include the CI-specific Tokenami types{ "extends": "./tsconfig.json", "include": [".tokenami/tokenami.env.ci.d.ts"] }
-
Use
tsc
with your new configurationtsc --noEmit --project tsconfig.ci.json
Tokenami provides some common default selectors for you but you can define your own custom selectors in the selectors
object of your config.
Use the ampersand (&
) to specify where the current element's selector should be injected:
const { createConfig, defaultConfig } = require('@tokenami/css');
module.exports = createConfig({
// ...
selectors: {
...defaultConfig.selectors,
'parent-hover': '.parent:hover > &',
},
});
Use by following the property spec:
<div className="parent">
<img src="..." alt="" />
<button style={css({ '--parent-hover_font-weight': 'bold' })} />
</div>
Selectors can also be combined with responsive rules:
<button style={css({ '--medium_parent-hover_font-weight': 'bold' })} />
Use an array value for custom selectors to generate nested rules:
module.exports = createConfig({
// ...
selectors: {
...defaultConfig.selectors,
hover: ['@media (hover: hover) and (pointer: fine)', '&:hover'],
},
});
This example will apply hover styles for users with a precise pointing device, such as a mouse, when --hover_{property}
is used.
Aliases allow you to create shorthand names for properties. When using custom aliases, the css
utility must be configured to ensure aliased properties are merged correctly across component boundaries.
Create a file in your project to configure the utility. You can name this file however you like, e.g. css.ts
:
// css.ts
import { createCss } from '@tokenami/css';
import config from '../.tokenami/tokenami.config';
export const css = createCss(config);
Use the css
utility exported from the file you created and it will handle aliases correctly.
The configuration expects the name of your new alias followed by an array of properties it maps to.
module.exports = createConfig({
// ...
aliases: {
p: ['padding'],
px: ['padding-left', 'padding-right'],
py: ['padding-top', 'padding-bottom'],
pt: ['padding-top'],
pr: ['padding-right'],
pb: ['padding-bottom'],
pl: ['padding-left'],
size: ['width', 'height'],
},
});
With the above config, px
is shorthand for padding-left
and padding-right
. This allows the css
utility to apply padding on the left and right when --px
is used.
Tokenami provides sensible defaults to restrict which values can be passed to properties based on your theme. For instance, --border-color
will only accept tokens from your color
object in theme, --padding
allows multiples of your grid, and --height
expects tokens from a size
key or multiples of your grid.
You can customise the default configuration by overriding the properties
object:
const { createConfig, defaultConfig } = require('@tokenami/css');
module.exports = createConfig({
theme: {
container: {
half: '50%',
},
pet: {
cat: '"🐱"',
dog: '"🐶"',
},
},
properties: {
...defaultConfig.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': 75 /* 300px with a 4px grid */,
'--height': 'var(--container_half)',
'--after_content': 'var(--pet_cat)',
})}
/>
Tokenami allows custom properties in the properties
config. This helps to create a configurable design system. For example, you can create --gradient-from
and --gradient-to
properties that accept color tokens to make reusable gradients:
module.exports = createConfig({
// ...
properties: {
...defaultConfig.properties,
'gradient-from': ['color'],
'gradient-to': ['color'],
},
});
And use the properties in your theme:
module.exports = createConfig({
theme: {
color: {
primary: '#f1f5f9',
secondary: '#334155',
},
surface: {
'radial-gradient':
'radial-gradient(circle at top, var(--gradient-from), var(--gradient-to) 80%)',
},
},
// ...
});
Now you can set different gradient stops when applying the gradient, and intellisense will suggest colours from your theme.
<div
style={css({
'--gradient-from': 'var(--color_primary)',
'--gradient-to': 'var(--color_secondary)',
'--background': 'var(--surface_radial-gradient)',
})}
/>
Theme values containing custom properties are not applied to theme selectors in the generated stylesheet. Tokenami must apply them to each element to ensure custom properties are inherited from the element's style attribute. This means theme modes cannot mix custom properties with differing hardcoded values/variables. For example:
// ❌
{
light: {
surface: {
gradient: 'linear-gradient(to right, red, var(--surface-to))';
}
}
dark: {
surface: {
gradient: 'linear-gradient(to right, blue, var(--surface-to))';
}
}
}
Instead, use matching custom properties and then use custom selectors to apply the different stops based on the theme mode:
// ✅
{
light: {
surface: {
gradient: 'linear-gradient(to right, var(--surface-from), var(--surface-to))';
}
}
dark: {
surface: {
gradient: 'linear-gradient(to right, var(--surface-from), var(--surface-to))';
}
}
}
<div
style={css({
'--light_surface-from': 'var(--color_white)',
'--dark_surface-from': 'var(--color_black)',
'--surface-to': 'var(--color_primary)',
'--background': 'var(--surface_gradient)',
})}
/>
This will ensure --surface-from
is set to var(--color_white)
in the light theme and var(--color_black)
in the dark theme.
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.
Note
Tokenami does not support browsers below the listed supported browser versions.
Integrating a design system built with Tokenami is straightforward. Include the tokenami.config.js
file and corresponding stylesheet from the design system in your project:
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'],
});
Tokenami will automatically generate styles and merge them correctly across component boundaries. See the example design system project and Remix project for a demo.
Before raising a bug, please double-check that it isn't already in my todo list. Some common pitfalls are listed below. If you need additional support or encounter any issues, please don't hesitate to join the Tokenami discord server.
In order to reduce style calculations at runtime, Tokenami puts the styles you write directly into the style attribute. This means the properties need to be valid style
syntax. Since the style attribute does not support media queries and psuedo selectors, Tokenami uses CSS variable syntax to enable them and therefore makes all properties consistently CSS variables for a reduced learning curve.
Additionaly, CSS properties applied to the style attribute have highest specificity making them hard to override. By using CSS variables instead, you can easily override Tokeanami's styles by adding a stylesheet after Tokenami's in your document.
This approach also helps you update styles directly in devtools because the syntax becomes second nature. The goal is for the double-dash to eventually feel as natural as typing custom class names in Tailwind. Instead of memorising many custom class names though, you can use CSS properties as usual, and intellisense will add the double-dash for you:
Tokenami is in early stages of development and currently only supports applications built using React (NextJS, Remix, etc.), Vue, or SolidJS.
Tokenami relies on cascade layers so only supports browsers with @layer
support.
Edge |
Firefox |
Chrome |
Safari |
iOS Safari |
Opera |
---|---|---|---|---|---|
99+ | 97+ | 99+ | 15.4+ | 15.4+ | 86+ |
- Paweł Błaszczyk (@pawelblaszczyk_)
A big thanks to:
- Tailwind CSS for inspiring most of the features in Tokenami
- Stitches for variants and responsive variants inspiration
- CSS Hooks for custom selectors inspiration
- Lightning CSS for generating the Tokenami stylesheet
Please do take the time to check these projects out if you feel Tokenami isn't quite right for you.