EffCSS v5: Self-Confident CSS-in-TS for Modern Frontend
Core API in v5.3.0
The library exports a small set of core at-rules utils:
variableandvariablescreate@propertyrulesanimationandanimationscreate@keyframesruleslayerandlayerscreate@layerrulescontainerandcontainerscreate@containerrulesfontandfontscreate@font-facerules
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 = {
[singleVariable]: '12px',
background: multipleVariables.primary(),
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': { fontFamily: multipleContainers.primary(multipleContainers.secondary) }
};
Besides at-rules, you can create arbitrary CSS rules with className or attribute - they just return selectors of different types:
import { className, attribute } from 'effcss';
const cls = className({
padding: '0.5rem',
'&:focus': { borderBottom: '4px solid grey' }
});
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.
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 resolverconfigureaffects style generation if it is called before the first stylesheet is createdserializeconverts 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!
Comments
No comments yet. Start the discussion.