Library / SDK
callstack/linaria avatar
callstack/linaria

callstack/linaria: Zero-Runtime CSS in JS Without the Evaluator Surprises

Zero-runtime CSS in JS library

12,351 stars413 forksTypeScriptMIT

At a glance

What is it?
Linaria extracts your styles to plain CSS files at build time, so nothing ships to the browser except class names. It is a good fit for React teams already comfortable with Babel or a bundler plugin, and a poor fit for anyone who wants styles computed at runtime.
Who is it for?
Adopt Linaria if you want authored-in-JS styles that compile to static CSS and your build pipeline already runs Babel or a bundler plugin. Do not adopt it if you need dynamic styles from the css tag, if you rely on side effects in modules imported into styles, or if you are pinned below Node.js 22.12.0.
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 51 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 September 30, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Linaria solves for React teams

Runtime CSS-in-JS libraries resolve styles in the browser. Every component that reads props to pick a colour does that work on the client, and the style objects travel in your JavaScript bundle. Linaria takes the opposite route: the README describes it as a "Zero-runtime CSS in JS library", and the feature list says CSS is "extracted to CSS files during build". What ships is a class name string, not a styling engine.

The audience is narrow and specific. You need a build step you control (webpack, esbuild, Rollup, Vite or Svelte are the integrations the README lists), and you need to be comfortable with the idea that your CSS is produced by evaluating your JavaScript modules at build time. Teams that already write template literals with the css or styled helpers will recognise the syntax immediately. Teams that want styles resolved per render, in the browser, from arbitrary runtime state, will not find that here.

How the build-time extraction actually works

Linaria 8 sits on top of WyW (@wyw-in-js/*). The README is explicit that Linaria "relies on WyW to evaluate your modules at build time and extract CSS". So the pipeline is: a bundler plugin hands your source files to WyW, WyW evaluates the modules, the css and styled template literals are collected, and the resulting CSS is emitted as a file while the JavaScript keeps only the generated class names.

The evaluation model changed in this major version. WyW 2 "defaults to eval.strategy: \"hybrid\", so statically provable values are resolved before falling back to the evaluator for dynamic values". In practice that means a value the tool can compute statically never triggers the evaluator, and only genuinely dynamic expressions fall through to it. The README frames this as a stability matter: if you see "slow builds, invalidation storms, or unexpected code being executed during the build", the cause is usually the WyW evaluation model and how your modules are structured.

Dynamic styles take a separate path. With the React bindings, prop-based values are applied through CSS custom properties, which is why the README lists no IE11 support for dynamic styles in styled components: CSS variables are the mechanism, not a fallback. The css tag itself does not support dynamic styles at all; the README points to docs/DYNAMIC_STYLES.md for alternative approaches.

Installing Linaria and writing a first styled component

The README gives the install command directly. It pulls in the core tag, the React bindings, and the Babel preset that WyW provides.

bash
npm install @linaria/core @linaria/react @wyw-in-js/babel-preset

Configuration is not in the README itself. The README says Linaria "is now built on top of wyw-in-js.dev" and that you should check the bundler guides on that site for webpack, esbuild, Rollup, Vite and Svelte. Treat that as the real setup step: the npm install is one line, the bundler wiring is where the work is.

Once configured, a component looks like this. Note that the interpolation for color is a function of props, which is the case Linaria routes through CSS variables.

js
import { styled } from '@linaria/react';
import { families, sizes } from './fonts';

const Title = styled.h1`
  font-family: ${families.serif};
`;

const Container = styled.div`
  font-size: ${sizes.medium}px;
  color: ${props => props.color};
  border: 1px solid red;

  &:hover {
    border-color: blue;
  }

  ${Title} {
    margin-bottom: 24px;
  }
`;

The README's own usage example then renders it as `<Container color="#333"><Title>Hello world</Title></Container>`. What you should see after a build is a CSS file containing the extracted rules and a component that renders with a generated class name, with the color arriving as a custom property rather than an inline style object.

For the plain css tag the shape is different, and imported values must be build-time constants:

js
import { css } from '@linaria/core';
import { modularScale, hiDPI } from 'polished';
import fonts from './fonts';

const header = css`
  text-transform: uppercase;
  font-family: ${fonts.heading};
  font-size: ${modularScale(2)};

  ${hiDPI(1.5)} {
    font-size: ${modularScale(2.5)};
  }
`;

The README's example then uses it as `<h1 className={header}>Hello world</h1>`. The functions from polished run at build time, not in the browser.

The side-effect rule is the constraint that bites

The README's trade-offs section states plainly that "Modules used in the CSS rules cannot have side-effects", and shows a colors.js import as the example. There must be no side effects in that file or in anything it imports. The recommended workaround is to move helpers and shared configuration into files that have none.

This is a harder constraint than it first reads. A design-token file that also registers a theme, patches a global, or initialises a logger is now a build-time hazard, because WyW evaluates the module to get the value. The README connects this to the stability discussion: unexpected code executing during the build is a symptom of the evaluation model meeting modules it cannot safely reason about. If your codebase has a shared utilities barrel file that does work on import, Linaria will find it.

Two other limits are worth stating before you commit. Dynamic styles are unavailable in the css tag, so a pattern built on passing props into a plain css template will not port. And dynamic styles in styled components depend on CSS custom properties, which rules out IE11.

Linaria versus runtime CSS-in-JS and versus plain CSS files

The clearest comparison is with runtime CSS-in-JS libraries such as styled-components and emotion. Those resolve styles in the browser and ship the styling logic in the bundle; Linaria resolves them during the build and ships CSS. The README does not treat them as mutually exclusive: the interoperability section says Linaria "can work together with other CSS-in-JS libraries out-of-the-box", with one caveat. Using Linaria styled components as selectors inside styled-components or emotion requires @linaria/interop, documented in packages/interop/README.md. That is a real integration cost, not a footnote.

The second comparison is against hand-written CSS or Sass with a preprocessor. Linaria keeps the authoring experience in the component file and lets you use JavaScript for logic, which the feature list frames as removing the need for a CSS preprocessor. It also supports using one anyway: Sass or PostCSS are listed as optional. The trade is that you gain colocation and lose nothing at runtime, but you take on a build-time evaluator as a dependency of your stylesheet.

If your styles are genuinely static and shared across a large app, plain CSS with a preprocessor remains simpler: no evaluation step, no side-effect rule, no bundler plugin to keep aligned. Linaria earns its place when styles are component-scoped and their values come from JavaScript modules.

Maintenance, version 8, and the MIT licence

The repository is not archived, and the last push was on 2026-08-10, which is recent enough that the project is being worked on. The most recent releases are [email protected], @linaria/[email protected] and @linaria/[email protected], all published on 2026-08-10. The core library and its stylelint integration are versioned in step, which simplifies keeping lint rules aligned with the compiler.

The upgrade cost is concentrated in the 8.x line. Linaria 8 requires Node.js >=22.12.0, which the README ties to the WyW 2 / Oxc dependency graph. If your CI or production build image runs an older Node, that is a hard blocker before any code change. The README also warns that if your build depends on "evaluator-only side effects or exact CSS rule order ties", you should review docs/MIGRATION_GUIDE.md. Rule order ties are the subtle one: with hybrid evaluation, statically provable values resolve earlier than they did under an evaluator-only strategy, and CSS order affects the cascade.

The licence is MIT, stated in the README badge and in the repository's package.json. That is permissive and standard for a library of this kind; the practical implication is that you can use and redistribute it, but you should read the LICENSE file yourself rather than take a summary as legal advice.

Editorial conclusion

Adopt Linaria if you want authored-in-JS styles that compile to static CSS and your build pipeline already runs Babel or a bundler plugin. Do not adopt it if you need dynamic styles from the css tag, if you rely on side effects in modules imported into styles, or if you are pinned below Node.js 22.12.0. Before committing, verify that your imported style modules are side-effect free, that your bundler integration is configured on wyw-in-js.dev, and that you have read docs/MIGRATION_GUIDE.md for the Linaria 8 evaluation changes.

Frequently asked questions

What is Linaria good for?

It is a zero-runtime CSS in JS library: styles written in css or styled template literals are extracted to CSS files during the build, so the browser receives class names rather than a styling runtime. It suits React teams that want component-scoped styles authored in JavaScript and are willing to configure a bundler plugin.

Which bundlers does Linaria support?

The README links to configuration guides for webpack, esbuild, Rollup, Vite and Svelte on the wyw-in-js.dev site, since Linaria 8 is built on top of WyW. The README itself does not repeat those setup steps.

Can I use dynamic styles with the css tag in Linaria?

No. The README's trade-offs section states that dynamic styles are not supported with the css tag and points to docs/DYNAMIC_STYLES.md for alternative approaches. Dynamic prop-based styles work through the React styled helper, which uses CSS custom properties.

What Node.js version does Linaria 8 require?

Linaria 8 requires Node.js >=22.12.0, which the README says is aligned with the WyW 2 / Oxc dependency graph. Older Node versions are not supported by this release line.

Can Linaria work alongside styled-components or emotion?

The README says Linaria can work together with other CSS-in-JS libraries out of the box. If you want to use Linaria styled components as selectors inside styled-components or emotion, you need @linaria/interop, documented in packages/interop/README.md.

Official sources

  1. callstack/linaria on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
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/callstack-linaria.svg)](https://hysenlabs.com/projects/callstack-linaria)