Guide
In this section, we'll install EffCSS and see how to use it.
Install
Type in your terminal:
# npm
npm i effcss
# pnpm
pnpm add effcss
# yarn
yarn add effcssCSS types
By default EffCSS is permissive: styles accept any object. To enable strict CSS-typing (autocompletion + validation of invalid values), define EffCSS.Properties once in your project via global namespace augmentation.
For example, you can install csstype
npm i -D csstypeThen combine with EffCSS
// env.d.ts
import type { Properties as CSSProperties } from 'csstype';
declare global {
namespace EffCSS {
interface Properties extends CSSProperties {}
}
}
export {};Now every style rule gets full TS autocompletion for CSS properties.
Use
To create styles with EffCSS, you just need to call the utilities. Fortunately, there are very few, and by their name you can understand what they do.
All style utilities are lazy by default: their styles are materialized only when a resolver is first used (resolver() or resolver + ''). If a styles reference other tokens, wrap them in a function () => ({...}) so it stays lazy.
Let's take a closer look at the usage of each of them.
id / className
id creates a single CSS rule with the specified content and returns the id resolver function:
import { id } from 'effcss';
const badge = id({ backgroundColor: 'red' });
badge(); // 'f_5-0'
'' + badge; // 'f_5-0'className creates a single CSS rule with the specified content and returns the classname resolver function:
import { className } from 'effcss';
const btn = className({ color: 'white' });
btn(); // 'f_6-0'
'' + btn; // 'f_6-0'Modifiers
Pass one or more modifiers to a selector resolver (id or className) to create variants of the same base rule. Each variant gets its own token, cached per modifier set. Pseudo-classes and attributes are appended to the selector; at-rules (@media, @container) wrap the rule at a different nesting level:
const box = className(() => ({ color: 'red' }));
box(); // 'f_6-0' -> `.f_6-0 { color: red; }`
box(':hover'); // 'f_6-1' -> `.f_6-1:hover { color: red; }`
box('[data-state="open"]'); // 'f_6-2' -> `.f_6-2[data-state="open"] { color: red; }`
box('@media (max-width: 700px)'); // 'f_6-3' -> `@media (max-width: 700px) { .f_6-3 { ... } }`
box('@media (max-width: 700px)', ':hover'); // 'f_6-4' -> `@media (...) { .f_6-4:hover { ... } }`The same base rule is reused across variants — conditions are applied where the rule is used, not baked into its definition.
Lazy styles & tokens
If your styles reference other tokens (resolvers), wrap them in a function () => ({...}). Passing an already-materialized token in a plain object throws a TypeError because it breaks lazyness and creates module side-effects:
const size = variable('12px');
// ❌ throws: rule contains a resolver result
// so it creates module side-effect
className({ padding: size() });
// ✅ lazy: token is resolved only when the rule is used
className(() => ({ padding: size() }));The function form keeps styles lazy — nothing is generated until the resolver is first called or coerced to a string. This applies to every utility that accepts an object with tokens: className, id, style and animation.
Native CSS nesting
Any style object supports nested selectors, so you can add pseudo-classes, attributes, children and combinators. & refers to the current selector:
const card = className({
padding: '1rem',
'&:hover': { boxShadow: '0 0 8px' }, // `.f_6-0:hover`
'&[data-active]': { borderColor: 'blue' }, // `.f_6-0[data-active]`
'& > .title': { fontWeight: 600 } // `.f_6-0 > .title`
});Inside functional forms you can even pass whole selectors (tokens) as nested keys — they are resolved lazily when the rule is used:
const card = className(() => ({
padding: '1rem',
['& > ' + btn]: { marginTop: '1rem' } // `& > .f_6-0`
}));variable
variable creates a single CSS @property rule and returns a variable resolver function:
import { variable } from 'effcss';
const accent = variable('#ff0');
'' + accent; // '--f_2-0'
accent(); // 'var(--f_2-0)'
accent('green'); // 'var(--f_2-0, green)'You can pass an object config to set syntax, inherits and initialValue:
const size = variable({ initialValue: '12px', syntax: '<length>', inherits: false });You can get and set the initial value of a variable using the corresponding methods:
const size = variable('12px');
size(); // materialize the variable
size.get(); // '12px'
size.set('24px'); // updates the @property rule
size.set((prev) => prev.replace('px', 'rem')); // functional updateINFO
The set/get methods only work after the variable is materialized (resolver() or resolver + ''). Before that, get() returns an empty string and set() has no effect.
animation
animation creates a single CSS @keyframes rule and returns a keyframes name resolver:
import { animation } from 'effcss';
const spin = animation({ from: { transform: 'rotate(0)' }, to: { transform: 'rotate(360deg)' } });
spin(); // 'f_3-0'
'' + spin; // 'f_3-0'
// function form when keyframes reference EffCSS tokens
const grow = animation(() => ({ from: { height: size() }, to: { height: '100px' } }));
grow(); // 'f_3-1'container
container creates a single CSS @container rule. String coercion gives the @container rule, calling gives name / type:
import { container } from 'effcss';
const card = container('inline-size');
'' + card; // '@container f_4-0'
card(); // 'f_4-0 / inline-size'
// without a type the default is used
const plain = container();
'' + plain; // '@container f_4-1'
plain(); // 'f_4-1 / normal'font
font creates a single CSS @font-face rule and returns a callable string with the bare family name and optional fallbacks:
import { font } from 'effcss';
const Inter = font({ src: "url('/inter.woff2')" });
Inter(); // 'f_1-0'
Inter('sans-serif'); // 'f_1-0, sans-serif'
Inter('Arial', 'sans-serif'); // 'f_1-0, Arial, sans-serif'layers
layers creates single CSS @layer declaration rule with all passed layers. It returns an object of layer resolver functions, one per layer name:
import { layers } from 'effcss';
const myLayers = layers(['reset', 'base']);
myLayers.reset(); // '@layer f_0-0'
'' + myLayers.reset; // '@layer f_0-0'
myLayers.base(); // '@layer f_0-1'style
style creates arbitrary styles and returns an empty string resolver function that injects them on the first usage:
import { style } from 'effcss';
const custom = style({ body: { margin: 0 } });
custom(); // '' (styles are injected)
'' + custom; // ''
// use function when using tokens inside style
const themed = style(() => ({ body: { color: accent(), ['& > ' + btn]: { marginTop: '1rem' } } }));
themed(); // ''style supports native CSS nesting and special values:
// array values are flattened into declarations
style(() => ({ body: { margin: ['0', 'auto'] } }));
// an array of objects yields several rules with the same key
// (works at any level)
style({
'@font-face': [
{ fontFamily: '"Bitstream Vera Serif Bold"', src: 'url("fira.woff2")' },
{ fontFamily: '"MyHelvetica"', src: 'local("Helvetica Neue Bold")', fontWeight: 'bold' }
]
});
// an empty string value emits a bare at-rule declaration
style(() => ({ '@layer custom': '' })); // @layer custom;
// camelCase properties are kebab-cased
style(() => ({ body: { fontFamily: 'serif' } }));theme
theme groups variables and their values into theme variants to simplify dynamic styles switching. It returns a controller object with vars, list, get and set methods:
import { theme } from 'effcss';
const myTheme = theme({
vars: { bg: '#eff', fg: 'black' },
options: {
light: { bg: '#eff', fg: 'black' },
dark: { bg: '#111', fg: 'white' }
},
initial: 'light'
});
myTheme.get(); // 'light'
myTheme.set('dark'); // true
myTheme.get(); // 'dark'
myTheme.vars.bg(); // 'var(--f_2-0)'
myTheme.vars.bg.get(); // '#111'
myTheme.vars.bg.set('#eee'); // override a single variableserialize
serialize converts all created stylesheets to an HTML string of <style> tags:
import { className, serialize } from 'effcss';
className({ color: 'white' })();
// render the generated `<style>` tags into your HTML
const html = serialize();
// '<style data-effcss-key="f-6" data-effcss-count="1">.f_6-0 { color: white; }</style>'This way, styles can be prepared on the server and reused on the client. This is especially useful for SSG/SSR.
setConfig / getConfig
getConfig returns current config of the global style context. setConfig changes config and affects style generation if it is called before the first stylesheet is created:
import { setConfig, getConfig } from 'effcss';
setConfig({ key: 'app' }); // must be called before the first usage
getConfig(); // { key: 'app' }setConfig returns true if applied, or false if the provider was already used. The key must start with a letter and contain only letters, digits, _ or -.
clear
clear removes all generated styles and resets counters:
import { clear } from 'effcss';
clear();context
context allows you to create a separate group of styles, returning an object with all the set of EffCSS utils excluding itself. Each context owns a unique key, counters and stylesheets, so tokens never collide across contexts:
import { context } from 'effcss';
const ctx = context({ key: 'app' });
const btn = ctx.className({ color: 'white' });
btn(); // 'app_6-0'
ctx.serialize(); // '<style data-effcss-key="app-6" data-effcss-count="1">...</style>'
ctx.getConfig(); // { key: 'app' }
ctx.clear(); // wipes styles, keeps the context usable
ctx.dispose(); // permanently destroys the contextgetContexts returns all active local contexts keyed by their key. Disposed and garbage-collected contexts are excluded (references are held weakly):
import { getContexts } from 'effcss';
const ctx = context({ key: 'app' });
getContexts(); // { app: ctx }Since context styles can be serialized and hydrated independently, you can use this feature to isolate styles between micro-frontends. A context also implements Symbol.dispose, so it can be used with the using statement.