What Is EffCSS?
EffCSS is a zero-dependency, framework-agnostic CSS-in-TS library that leverages native browser APIs (CSSStyleSheet, adoptedStyleSheets) instead of runtime injection or build plugins. Version 5 is a complete rethink that moves away from the useStyleProvider / TStyleSheetMaker pattern of v4 toward standalone utility functions. The new version gets rid of unnecessary abstractions in favor of a simple API.
Core API in v5.3.0
The library exports a small set of core at-rules utils:
-
variableandvariablescreate @property rules, -
animationandanimationscreate @keyframes rules, -
layerandlayerscreate @layer rules, -
containerandcontainerscreate @container rules, -
fontandfontscreates @font-face rules.
They can be used as follows:
import {
variable, variables,
animation, animations,
layer, layers,
container, containers,
font, fonts
} from 'effcss';
// variables
const singleVariable = variable('10px');
const multipleVariables = variable({
primary: {
syntax: 'color',
inherits: false,
initialValue: '#2192a7'
},
secondary: 'black'
});
const variableStyles = {
// override value
[singleVariable]: '12px',
// use value
background: multipleVariables.primary(),
// use value with fallback
color: multipleVariables.secondary('black')
};
// animations
const singleAnimation = animation({
from: {
transform: 'rotate(0deg)',
},
to: {
transform: 'rotate(360deg)',
},
});
const multipleAnimations = animations({
simple: {
'50%': {
visibility: 'hidden'
}
},
smooth: {
'0%': {
opacity: 1
},
'50%': {
opacity: 0
},
'100%': {
opacity: 1
}
}
});
const animationStyles = {
'.spin': {
animation: `200ms ${singleAnimation}`
}
'.blink': {
animationName: multipleAnimations.smooth,
animationDuration: '3s'
}
};
// layers
const singleLayer = layer();
const multipleLayers = layers(['theme', 'layout', 'utilities']);
const layerStyles = {
[singleLayer]: {
button: {
border: 'none'
}
}
[multipleLayers.theme]: {
body: {
background: 'white'
}
}
};
// containers
const singleContainer = container();
const multipleContainers = containers({
normal: '',
inline: 'inline-size',
scrollState: 'size scroll-state'
});
const containerStyles = {
'.container-1': {
container: singleContainer()
},
[singleContainer + ' (max-width: 768px)']: {
'.offset-s': {
width: '8px'
}
},
'.container-2': {
container: multipleContainers.inline()
},
[multipleContainers.inline + ' (max-width: 768px)']: {
'.offset-s': {
width: '4px'
}
}
};
// fonts
const singleFont = font({
src: `url("https://mdn.github.io/shared-assets/fonts/FiraSans-Regular.woff2")`,
genericName: 'sans-serif'
});
const multipleFonts = fonts({
primary: {
src: `url("/fonts/roboto-regular.woff2") format("woff2"), url("/fonts/roboto-regular.woff") format("woff")`,
weight: 400,
style: 'normal',
display: 'swap'
},
secondary: {
src: `url("https://mdn.github.io/shared-assets/fonts/FiraSans-Regular.woff2")`
}
});
const fontStyles = {
body: {
fontFamily: globalFonts.first()
},
'.font-primary': {
// secondary as fallback
fontFamily: multipleContainers.primary(multipleContainers.secondary)
}
};
Besides at-rules, you can create an arbitrary CSS rules with className or attribute - they just return selectors of different types:
import { className, attribute } from 'effcss';
// returns string
const cls = className({
padding: '0.5rem',
'&:focus': {
borderBottom: '4px solid grey'
}
});
// returns object
const attr = attribute({
margin: 'auto',
'&:hover': {
outline: '2px solid black',
'.child': {
background: 'grey'
}
}
});
export const Component = () => {
return <div {...attr} className={cls}>
...
</div>
};
In addition, you can create entire stylesheets using the classNames, attributes and customStyles utilities. The first two implement the contract-first approach, let's look at this approach in more detail.
Contract-first approach
1. Declare a Type Contract
You describe your design system as a TypeScript type. This becomes the source of truth for both implementation and consumption:
type Components = {
rounded: true;
h: 'full' | 'half';
card: {
bg: 'primary' | 'secondary';
disabled: boolean;
};
spinner: {};
};
type Utils = {
w: 's' | 'm' | 'l';
spacing: 0 | 1 | 2;
blink: true;
};
2. Implement with attributes or classNames
attributes generates scoped data-attribute selectors (returns a props object for spread):
const styleComponents = attributes<Components>((selectors) => {
const { rounded, card, spinner } = selectors;
return {
[rounded.true]: { borderRadius: '50%' },
[spinner]: { animation: `${spinAnimation} infinite 6s linear` },
[card]: { display: 'flex', justifyContent: 'center' },
[card.bg.primary]: { background: colors.primary() },
[card.bg.secondary]: { background: colors.secondary('cyan') },
};
});
classNames generates class selectors (returns a className string):
const styleUtils = classNames<Utils>((selectors) => {
const { w, blink } = selectors;
return {
[blink.true]: { animation: `${blinkAnimations.smooth} 2s infinite` },
[w.s]: { width: widthVars.s() },
[w.m]: { width: widthVars.m() },
[w.l]: { width: widthVars.l() },
};
});
3. Apply
const cardAttrs = styleComponents({ card: { bg: 'primary', disabled: true } });
const utilsCls = styleUtils({ w: 'm' });
export const App = () => (
<div {...cardAttrs} className={utilsCls}>...</div>
);
Other utils
There are also other specialized utilities:
-
stylesheetreturns the created stylesheet by resolver, -
configureaffects style generation if it is called before the first stylesheet is created, -
serializeconverts its argument or all created stylesheets to an HTML string.
Key Strengths
| Aspect | Benefit |
|---|---|
| Zero dependencies | No runtime overhead, no build plugins, no PostCSS config |
| Framework agnostic | Works identically in React, Vue, Svelte, Solid, Lit, Angular, vanilla TS |
| TypeScript contract | Autocompletion for selectors — impossible to misspell a variant |
| Selector isolation | Unique scoped names/attrs out of the box; optional minification |
| SSR ready |
serialize() dumps stylesheets to HTML strings with zero extra config |
| Browser-native | Uses CSSStyleSheet + adoptedStyleSheets — no style tag injection |
When to Use EffCSS
- Design-system libraries — the type-contract pattern lets you expose a typed API while hiding implementation.
- Component libraries — scoped selectors eliminate style leaks without CSS Modules or Shadow DOM.
- Projects that value TypeScript DX — autocompletion for every modifier/value reduces context-switching.
- Micro-frontends / multi-team setups — each stylesheet is fully isolated by default.
When to Consider Alternatives
- You're already deep in Tailwind — EffCSS is a different paradigm (component-scoped vs. utility-first). They can coexist, but Tailwind's ecosystem is larger.
- You want a build-time zero-runtime solution (Linaria, vanilla-extract) — EffCSS has a small runtime (~3.7 KB minzipped size) for stylesheet management.
- You need CSS Modules — if your team is already comfortable with them and doesn't need TypeScript-driven selectors.
- Pure CSS solves all your problems – and that's great!
Final thoughts
EffCSS v5 fills a specific niche: type-safe, isolated, framework-agnostic styling with zero tooling overhead. It's not a Tailwind killer or a styled-components replacement — it's a different approach where the TypeScript compiler becomes your style guide enforcer. If you value compile-time safety and want styles that "just work" across any framework, it's worth a serious look.
In any case, I would be interested to know whether you use CSS-in-JS, what attracts you to it or, on the contrary, repels you.
Enjoy your Frontend Development!
Top comments (0)