EffCSS v6: Self-Confident CSS-in-TS for Reimagined Frontend

The first release of EffCSS was almost two years ago — back then, it seemed that simply leveraging browser APIs and creating some basic styles would be enough to build a good tool. However, the architectural constraints of the library gradually led it toward a more restrained and balanced API.
EffCSS v5 introduced the concept of "lazy" styles — where CSS is generated only upon first use, but this approach presented several challenges:
- How should the lazy generation mode be controlled?
- How can lazy and synchronous CSS creation modes be reconciled?
- Most importantly, how can we ensure that the build tool is still able to remove unused Effcss styles from the bundle?
Creating styles via EffCSS v5 utility calls introduced side effects at the module level. A conditional "lazy" mode could mislead an inexperienced user, while the public API had grown excessively large — daunting users with its various naming conventions (e.g., className / classNames / lazyClassName / lazyClassNames).
It is also worth noting that the concept of independent stylesheets proved to be quite cumbersome: to take advantage of TypeScript’s autocompletion, one had to define the entire stylesheet, even if only a few classes were actually being used.
After analyzing all these issues, I realized it was time for a new major release — one that sheds everything acting as dead weight or a constraint. Meet EffCSS v6.
Tree-shakeable and lazy by default
The new version is built on a few simple ideas that stay consistent across every utility:
- Unified token-based. Style utilities return token resolver functions.
- Lazy by default. Styles are materialized only when a token resolver is first used (
resolver()orresolver + ''). If a rule references other tokens, wrap it in a function() => ({...})so it stays lazy. - Tree-shakeable. Each utility is a standalone export; importing only what you need keeps the bundle small.
- Native CSS nesting.
&refers to the current selector, no extra syntax over CSS. - Tokens as keys / values. Inside functional forms you can pass all EffCSS tokens (selectors, variables, animations, etc), resolved lazily when the rule is used.
The tree-shakeability is easy to verify: the bundle size grows only with the utilities you actually import. Importing a single utility keeps the bundle tiny, and even the whole library stays compact:
| Import | Size |
|---|---|
only variable |
3398 B (3.32 KB) |
only className |
3606 B (3.52 KB) |
only theme |
3927 B (3.83 KB) |
all utils |
7422 B (7.25 KB) |
Minimalistic API
The public API is deliberately small and consistent. Every utility is a standalone export, and they all follow the same "token resolver" pattern:
| Utility | Description | Returns |
|---|---|---|
id / className |
Unique #id / .class selector rule |
selector resolver |
variable |
CSS custom property (@property) |
variable resolver |
animation |
@keyframes rule |
name resolver |
container |
@container rule |
container resolver |
font |
@font-face rule |
font-family resolver |
layers |
@layer declaration |
object of layer resolvers |
style |
Arbitrary styles | styles resolver |
theme |
Group variables into theme variants | theme controller |
serialize |
Serialize styles into <style> tags |
string |
clear |
Remove all styles and reset counters | void |
setConfig / getConfig |
Set / get the global context config | boolean / Settings |
context / getContexts |
Create / list isolated style contexts | Context / Record<string, Context> |
The most important changes in the API:
- There are no longer any utilities for generating data attributes; while that might have been convenient, it certainly wasn't the best idea from a semantic standpoint — in any case, CSS classes and IDs are more than sufficient for creating selectors.
- No more metadata during serialization — essentially, you only need serialization to prepare styles in advance, before client-side rendering; any additional strings in the HTML would simply increase network traffic.
- There is no explicit access to the stylesheets — this capability was essentially an "emergency hatch" that allowed for interference with how EffCSS operates.
- Explicit management of the state and lifecycle of each style context — so that you can explicitly clean up the resources that have been created.
Tailwind-style modifiers
Selector resolvers (id and className) accept one or more modifiers 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, while 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('@media (max-width: 700px)'); // 'f_6-2' -> `@media (...) { .f_6-2 { ... } }`
box('@media (max-width: 700px)', ':hover'); // 'f_6-3' -> `@media (...) { .f_6-3:hover { ... } }`
The same base rule is reused across variants — conditions are applied where the rule is used, not baked into its definition, unlike native CSS nesting.
Complex and arbitrary styles
Beyond single-rule generators, EffCSS provides two utilities for complex styles: style and theme.
style creates arbitrary styles and returns an empty string resolver that injects them on the first usage. Inside functional forms you can reference other selectors (tokens) as nested keys — resolved lazily when the rule is used:
const btn = className({ color: 'white' });
const themed = style(() => ({
body: {
color: accent(),
['& > ' + btn]: { marginTop: '1rem' } // `& > .f_6-0`
}
}));
themed(); // ''
Because the rule is wrapped in a function, the token btn is resolved only at the moment of materialization — keeping the styles lazy and avoiding module-level side effects.
theme groups variables and their values into theme variants to simplify dynamic style switching. It returns a controller with vars, list, get and set:
const myTheme = theme({
vars: { bg: '#eff' },
options: { light: { bg: '#eff' }, dark: { bg: '#111' } },
initial: 'light'
});
myTheme.get(); // 'light'
myTheme.set('dark'); // true
Isolated style contexts
context() creates an isolated style generation context with its own key, counters and stylesheets, so tokens never collide across contexts:
import { context } from 'effcss';
const ctx = context({ key: 'app' });
ctx.className({ color: 'white' })(); // 'app_6-0'
ctx.clear(); // wipes styles, keeps the context usable
ctx.dispose(); // permanently destroys the context
Each context exposes the full set of utilities (except context and getContexts) plus explicit lifecycle management (clear, dispose, Symbol.dispose for using). getContexts() lists active contexts, excluding disposed ones via weak references — ideal for isolating styles between micro-frontends.
SSR is kept
SSR support is preserved. Every context — the global one and each isolated one — can be serialized into <style> tags and reused on the client:
import { className, serialize } from 'effcss';
className({ color: 'white' })();
const html = serialize();
// '<style data-effcss-key="f-6" data-effcss-count="1">.f_6-0 { color: white; }</style>'
Isolated contexts serialize independently via ctx.serialize(), so micro-frontends can prepare and hydrate their own styles without collisions.
Who is it for
- Design-system and component libraries — typed, isolated selectors with no style leaks.
- Micro-frontends / multi-team setups — each context is fully isolated and independently serializable.
- SSR/SSG projects — styles are prepared on the server and hydrated on the client without FOUC.
- Anyone who wants a small, lazy, tree-shakeable CSS-in-TS — the whole library is ~7 KB.
If you need a build-time zero-runtime solution (Linaria, vanilla-extract, CSS-Zero) or are already deep in Tailwind, EffCSS is a different paradigm — but it can coexist with both.
Final thoughts
EffCSS v6 is a leaner, more confident library. By unifying the API around token resolvers, making everything lazy and tree-shakeable by default, it removes the abstractions that weighed v5 down while keeping the core idea intact — more flexibility with lower costs. Could this be the tool you were looking for?
Enjoy your Frontend Development!
About the author
- Frontend Developer
