Open-source project
vanilla-extract-css/vanilla-extract avatar
vanilla-extract-css/vanilla-extract

vanilla-extract: TypeScript Stylesheets With No Runtime

Zero-runtime Stylesheets-in-TypeScript

10,434 stars359 forksTypeScriptMIT

At a glance

What is it?
vanilla-extract compiles .css.ts files into static CSS at build time, giving you locally scoped class names and CSS variables without shipping a styling runtime. Here is how the build pipeline works, where it stops being the right tool, and what to check before adopting it.
Who is it for?
Adopt vanilla-extract when you want typed, locally scoped styles and are willing to wire a build plugin into Vite, webpack, Next.js or another bundler; the README points to the documentation site for setup guides and to examples/webpack-react and examples/next for working configurations.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 5 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem vanilla-extract solves: typed styles that compile away

Most CSS-in-JS libraries resolve styles in the browser. That means a runtime, a style injection step, and a bundle that grows with every component. vanilla-extract takes the other path: you write styles in .css.ts files, the build tool evaluates those files, and the output is a static stylesheet. The README describes it as "CSS Modules-in-TypeScript" with scoped CSS variables and more, and states plainly that none of the code in .css.ts files is included in the final bundle. Think of TypeScript as the preprocessor instead of Sass or Less.

The audience is narrow but real. If you already like CSS Modules because class names are hashed and local, but you want autocomplete on property names, compile-time errors when you misspell a value, and variables that are scoped rather than global, this is aimed at you. It also suits design-system authors, because the theme system is built on CSS variables and the README claims support for simultaneous themes with no globals. Teams that only need a few utility classes will find the setup cost hard to justify.

How .css.ts files become static CSS: the build-time mechanism

The unit of work is the .css.ts file. Inside it you import createTheme and style from @vanilla-extract/css. createTheme returns a tuple: a class name for the theme and a vars object whose properties map to CSS custom properties. Passing vars.color.brand into style() emits a var() reference in the generated CSS rather than the literal value, which is what makes theme swapping possible without regenerating styles.

The evaluation happens in the bundler plugin, not in your application code. The README says that once build tooling is configured, these files are evaluated at build time. The repository ships a packages/ directory with per-bundler plugins, and the release feed lists @vanilla-extract/vite-plugin, @vanilla-extract/turbopack-plugin and @vanilla-extract/sprinkles as separately versioned packages, so each integration has its own release cadence. The examples/ directory contains webpack-react and next projects that show the wiring in practice.

Two optional escape hatches exist. The README mentions an optional runtime version for development and testing, and an optional API for dynamic runtime theming. Those are opt-in, and reaching for them changes the zero-runtime property that gives the project its name.

Installing vanilla-extract and writing a first themed style

The README points to the documentation site for setup guides rather than listing install commands, so the package names below come from the imports shown in the README and from the workspace packages. The core package is @vanilla-extract/css. Start by adding it and the plugin for your bundler.

bash
pnpm add @vanilla-extract/css
pnpm add -D @vanilla-extract/vite-plugin

The README does not print a Vite config, so treat the plugin registration as the step to confirm against the documentation site for your bundler before you rely on it. Once the plugin is registered, create a .css.ts file. This example is copied from the README:

ts
// styles.css.ts
import { createTheme, style } from '@vanilla-extract/css';

export const [themeClass, vars] = createTheme({
  color: { brand: 'blue' },
  font: { body: 'arial' }
});

export const exampleStyle = style({
  backgroundColor: vars.color.brand,
  fontFamily: vars.font.body,
  color: 'white',
  padding: 10
});

Consume the exported names in your markup, again as the README shows:

ts
// app.ts
import { themeClass, exampleStyle } from './styles.css.ts';

document.write(`
  <section class="${themeClass}">
    <h1 class="${exampleStyle}">Hello world!</h1>
  </section>
`);

What you should see after a build is a generated CSS file containing hashed class names and custom properties such as the brand colour, with no styling code left in the JavaScript bundle. If the .css.ts file is not evaluated and no CSS appears, the plugin is not registered for that bundler.

Where the build-time model breaks down

The zero-runtime promise has a cost: anything that depends on runtime state cannot be expressed as a plain style object. A colour picked from a user profile, an offset measured from a DOM node, or a value fetched from an API is not known when the build runs. The README acknowledges this by offering an optional API for dynamic runtime theming, but using it means you are no longer on the zero-runtime path, and the documentation does not describe the performance profile of that mode.

The second constraint is tooling. Every .css.ts file is inert until a bundler plugin evaluates it. That makes the build configuration part of your critical path: a misconfigured plugin produces no styles rather than an error you can see in the browser. The repository keeps separate plugins per bundler, so the integration you depend on has its own release history. The last push to the repository was on 2026-08-27, and the most recent release in the feed is @vanilla-extract/[email protected] on the same date, which tells you the project is still receiving commits, but the README does not document a support matrix for framework versions, so matching your framework to the fixtures in examples/ is on you.

vanilla-extract compared with CSS Modules and runtime CSS-in-JS

The closest comparison is CSS Modules, which the README names directly as the concept it extends. Both hash class names so styles stay local, and both produce static CSS. The difference is what you write. With CSS Modules you author .module.css and get no type checking on property names or values; with vanilla-extract you author .css.ts and the TypeScript compiler checks the object you pass to style(). The theme system is the other gap: CSS Modules variables are global by convention, while createTheme returns a scoped vars object.

Against runtime CSS-in-JS libraries, the trade is reversed. A runtime library lets you compute a style from props at render time and inject it, which vanilla-extract cannot do without the optional runtime API. In exchange, vanilla-extract ships no styling runtime, so the JavaScript bundle does not carry style objects or an injection layer. The README also points to Sprinkles, its atomic CSS framework built on top of vanilla-extract, for teams that want a utility-class workflow with the same build-time guarantees.

Maintenance, versioning and the MIT licence

The repository is a pnpm workspace with packages, examples, fixtures, tests and a site directory, and it uses Changesets for releases: the version script runs changeset version, and release runs changeset publish. That explains why @vanilla-extract/vite-plugin and @vanilla-extract/sprinkles carry independent version numbers rather than moving together. For adopters, the practical consequence is that upgrading the core package and a plugin are separate decisions, and a plugin release may land on a different date from the core.

The last push was on 2026-08-27, so the project has recent activity. The repository is not archived. Maintenance cost for you is mostly build configuration: when you change bundlers or major framework versions, the plugin is what needs checking. The licence is MIT, which permits commercial use and modification provided the copyright notice and permission notice are retained; this is a description of the licence text, not legal advice, and you should read LICENSE in the repository for the exact terms.

Editorial conclusion

Adopt vanilla-extract when you want typed, locally scoped styles and are willing to wire a build plugin into Vite, webpack, Next.js or another bundler; the README points to the documentation site for setup guides and to examples/webpack-react and examples/next for working configurations. Do not adopt it if you need styles computed from state that is only known at runtime, or if your team will not maintain build configuration, because every .css.ts file depends on that plugin being present. Before committing, verify the plugin package for your bundler exists under packages/ and is published, confirm your framework version against the fixtures in examples/, and check the docs for the escape hatch you need for dynamic values. The MIT licence imposes no distribution conditions beyond keeping the notice.

Frequently asked questions

How do I use vanilla-extract in a project?

Write styles in .css.ts files using style() and createTheme() from @vanilla-extract/css, then configure your bundler plugin so those files are evaluated at build time. The README states that once build tooling is configured, the .css.ts files are evaluated at build time and none of their code ends up in the final bundle.

Is vanilla-extract available on npm?

Yes. The README imports from @vanilla-extract/css, and the release feed lists separately published packages including @vanilla-extract/vite-plugin, @vanilla-extract/turbopack-plugin and @vanilla-extract/sprinkles.

Does vanilla-extract work with any front-end framework?

The README states that it works with any front-end framework, or even without one, because the output is static CSS. The repository ships examples for webpack-react and Next.js, and separate plugin packages for Vite and Turbopack.

What is the difference between vanilla-extract and CSS Modules?

The README describes vanilla-extract as CSS Modules-in-TypeScript, with the additions of locally scoped CSS variables, @keyframes and @font-face rules plus a theme system. Both produce static CSS with locally scoped class names, but vanilla-extract styles are written in TypeScript and type-checked.

Does vanilla-extract have a runtime?

The default build path has no styling runtime: styles are generated as static CSS at build time. The README also documents an optional runtime version for development and testing and an optional API for dynamic runtime theming, which are opt-in.

Official sources

  1. License: MIT
  2. Project website
  3. README
  4. Releases
  5. vanilla-extract-css/vanilla-extract on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/vanilla-extract-css-vanilla-extract.svg)](https://hysenlabs.com/projects/vanilla-extract-css-vanilla-extract)