nano-css
nano-css is a modular runtime CSS-in-JS renderer. The core create() function gives you put(), which converts JavaScript style objects into CSS rules and inserts them into a browser style sheet or accumulates them as a string on the server. Generated classes, style sheets, React components, nesting, keyframes, prefixing, right-to-left conversion, hydration, and extraction all come from separate addons or presets. That makes the core small, but a real application must choose and initialize a styling stack.
nano-css earns its name at the core, but the useful product is an addon graph you own and test. Keep it for existing v5 systems or unusually custom renderers; new React projects usually get a clearer maintenance story from goober, Emotion, or a build-time option.
Use it if
- You want runtime CSS-in-JS without wrapper components and prefer direct CSS rule insertion over inline style objects
- You need a renderer that can work without React and are willing to install only the addons your application uses
- You want stable generated class names, server-side CSS collection, and client hydration from one low-level system
- You are maintaining an existing nano-css v5 codebase and need its rule, sheet, jsx, styled, atoms, or RTL APIs
- You want a plug-and-play styling library: only put() is installed by the core, while rule(), sheet(), stable names, nesting, keyframes, prefixing, hydration, and framework helpers require presets or correctly ordered addons
- You chose it only for the README's 0.5 KB claim: that link measures nano-css 1.15.3, while current v5 installs eight runtime dependencies and feature presets add more code paths
- Your SSR output relies on media queries or keyframes being recognized during hydration: the hydrate documentation explicitly says those two rule types are not hydrated
- You need invalid production CSS to fail loudly: the v5 core catches and discards insertRule errors in production, so a bad selector can disappear without an exception
- You prefer build-time or zero-runtime styles for React Server Components, strict Content Security Policy, or minimal client JavaScript: nano-css creates and mutates a style element at runtime unless you build a separate extraction process
- Your legal policy rejects public-domain dedication licenses: nano-css uses the Unlicense rather than a conventional MIT or Apache-2.0 grant
Setup reality
npm install nano-css does not give you the README's full feature list. The core exposes create() and preinstalls only put(); common tasks such as generated classes, named sheets, stable hashing, advanced nesting, keyframes, React elements, prefixing, SSR hydration, and extraction are separate addons. The sheet preset installs stable, nesting, atoms, keyframes, rule, sheet, and development source maps. The React preset installs a much larger set and uses React.createElement. Although the core describes itself as framework-agnostic, the published v5.6.2 metadata declares non-optional react and react-dom peer dependencies with wildcard versions, which can surprise standalone consumers under modern npm peer installation. Browser use appends a style element to document.head unless you provide one through the sh option; production uses insertRule(), while development writes readable text nodes and creates an extra test sheet. Set NODE_ENV correctly or production behavior and size will differ. For SSR, create a renderer for the request, execute every lazy sheet rule you need, embed nano.raw, install stable naming, and add the hydrate addon on the client with the matching style element. Reusing one server renderer keeps accumulating raw CSS. Hydration does not cover media queries or keyframes. Addons wrap renderer methods, so installation order and documented dependencies matter. The extract addon is only a low-level primitive, not a complete bundler integration, and TypeScript coverage varies across addon entry points.
Patterns
Create a renderer and inject a selectorinject-global-rule
import { create } from 'nano-css';
const nano = create({ pfx: 'acme-' });
nano.put('.notice', {
color: 'tomato',
border: '1px solid currentColor',
});put() is the only styling method installed by the core package. In a browser it creates a style element in document.head unless you pass one as sh.
Generate a class from a style objectgenerate-class-name
import { create } from 'nano-css';
import { addon as addonStable } from 'nano-css/addon/stable';
import { addon as addonRule } from 'nano-css/addon/rule';
const nano = create();
addonStable(nano);
addonRule(nano);
const className = nano.rule({ color: 'tomato' });rule() returns a class name with a leading space so it can be concatenated. Install stable before rule when server and client must hash styles identically.
Define several lazily inserted classescreate-style-sheet
import { preset } from 'nano-css/preset/sheet';
const { sheet } = preset({ pfx: 'acme-' });
const styles = sheet({
input: { border: '1px solid #aaa' },
button: { color: 'white', background: 'navy' },
}, 'ContactForm');
button.className = styles.button;sheet() does not insert a rule until its property is accessed. Touch every class needed by an SSR response before reading nano.raw.
Use parent references and grouped selectorsnest-selectors
import { preset } from 'nano-css/preset/sheet';
const nano = preset();
nano.put('.menu', {
'&:hover': { color: 'blue' },
'.icon, .label': { opacity: 0.8 },
'.dark &': { color: 'white' },
});The ampersand and comma interpolation shown here come from the nesting addon included by the sheet preset, not from bare create() alone.
Generate a responsive classadd-media-query
import { preset } from 'nano-css/preset/sheet';
const { rule } = preset();
const card = rule({
display: 'grid',
gridTemplateColumns: '1fr 1fr',
'@media (max-width: 640px)': {
gridTemplateColumns: '1fr',
},
}, 'Card');Server output can contain media queries, but the hydrate addon does not recognize media-query rules as already present on the client.
Create a uniquely named animationcreate-keyframes
import { preset } from 'nano-css/preset/sheet';
const { keyframes, rule } = preset();
const spin = keyframes({
'0%': { transform: 'rotate(0deg)' },
'100%': { transform: 'rotate(360deg)' },
});
const spinner = rule({ animation: `${spin} 800ms linear infinite` });The keyframes addon emits prefixed variants by default. Its rules are another documented gap in client hydration.
Build a prop-driven React buttoncreate-react-component
import { preset } from 'nano-css/preset/react';
const { styled } = preset({ pfx: 'acme-' });
const Button = styled.button(
{ border: 0, padding: '8px 12px' },
(props) => ({
color: 'white',
background: props.danger ? 'crimson' : 'royalblue',
}),
'Button',
);The React preset installs many addons and uses React.createElement. Semantic props such as danger can reach the DOM unless your component layer filters them.
Apply one-off styles through a styling blockoverride-component-styles
import { preset } from 'nano-css/preset/react';
const { jsx } = preset();
const Panel = jsx('section', { padding: '16px', background: '#fff' }, 'Panel');
export function Warning() {
return <Panel css={{ borderLeft: '4px solid orange' }}>Check input</Panel>;
}The css prop is consumed by nano-css and is not passed to the element. The $as and $ref props are also reserved by the jsx addon.
Collect CSS during server renderingrender-server-css
import { preset } from 'nano-css/preset/sheet';
export function renderStyles() {
const nano = preset({ pfx: 'acme-' });
const title = nano.rule({ fontWeight: 700 }, 'Title');
const markup = `<h1 class="${title}">Hello</h1>`;
return `${markup}<style id="nano-css">${nano.raw}</style>`;
}Create an isolated renderer per request or reset it deliberately; raw is an accumulating string. Escape or control all generated HTML values in real templates.
Reuse a server-generated style elementhydrate-server-css
import { create } from 'nano-css';
import { addon as addonHydrate } from 'nano-css/addon/hydrate';
const style = document.getElementById('nano-css');
const nano = create({ sh: style, pfx: 'acme-' });
addonHydrate(nano);Use the same prefix, style definitions, and stable naming on both sides. Existing media queries and keyframes are not hydrated by this addon.
Add vendor prefixes to emitted declarationsprefix-browser-properties
import { create } from 'nano-css';
import { addon as addonPrefixer } from 'nano-css/addon/prefixer';
const nano = create();
addonPrefixer(nano);
nano.put('.layout', { display: 'flex', userSelect: 'none' });Prefixing is not part of the base renderer. This addon uses inline-style-prefixer, one of nano-css v5's runtime dependencies.
Convert a renderer to RTL outputflip-right-to-left
import { create } from 'nano-css';
import { addon as addonRtl } from 'nano-css/addon/rtl';
const rtlNano = create({ pfx: 'rtl-' });
addonRtl(rtlNano);
rtlNano.put('.card', { marginLeft: '12px', textAlign: 'left' });The RTL addon wraps put() and transforms every style object through rtl-css-js. Use a separate renderer if the same page emits both LTR and RTL rules.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| goober | npm | You want a small CSS-in-JS API with less addon assembly and straightforward React or framework integration |
| @emotion/css | npm | You want a mature generated-class API, composition, SSR support, and a larger documentation ecosystem |
| styled-components | npm | Your React team prefers component-scoped styles and accepts a larger runtime abstraction |
| @vanilla-extract/css | npm | You want typed styles extracted at build time with no runtime style injection |