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)
}
};
Enter fullscreen mode Exit fullscreen mode
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>
};
Enter fullscreen mode Exit fullscreen mode
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;
};
Enter fullscreen mode Exit fullscreen mode
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') },
};
});
Enter fullscreen mode Exit fullscreen mode
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() },
};
});
Enter fullscreen mode Exit fullscreen mode
3. Apply
const cardAttrs = styleComponents({ card: { bg: 'primary', disabled: true } });
const utilsCls = styleUtils({ w: 'm' });
export const App = () => (
<div {...cardAttrs} className={utilsCls}>...</div>
);
Enter fullscreen mode Exit fullscreen mode
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 readyserialize() 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!
답글 남기기