Lazy mode
By default EffCSS generates CSS eagerly: a stylesheet rules are written into the CSSOM as soon as you call a utilities. In lazy mode the actual generation is deferred — CSS lands in the stylesheet only when the produced resolver is actually used (called or string-coerced).
Lazy mode is available in two flavors, so you can pick the level of control you need:
| Flavor | Trigger | Scope |
|---|---|---|
| Controlled | opt-in lazy* utilities | per-utility, always lazy |
| Library-level | configure({ lazy: true }) | every generating utility that returns a resolver |
They work independently and can be combined.
1. Controlled — lazy* utilities
These utilities are always lazy, regardless of any global configuration. Each call returns a function-resolver. Nothing is generated until you:
- call the resolver —
fn()(orfn(...args)), - coerce it to a string —
`${fn}`orString(fn).
The rule content may be passed either as an object or as a function that returns the object. A function is evaluated lazily, on the first warm-up.
| Utility | Returns | Generates |
|---|---|---|
lazyClassName(rule) (or className.lazy(rule)) | () => string (class name) | one anonymous class rule |
lazyAttribute(rule) (or attribute.lazy(rule)) | () => object ({ 'data-…': '' }) | one anonymous attribute rule |
lazyClassNames(gen) (or classNames.lazy(gen)) | selectors resolver | a class-selector stylesheet |
lazyAttributes(gen) (or attributes.lazy(gen)) | selectors resolver | an attribute-selector stylesheet |
lazyCustomStyles(gen) (or customStyles.lazy(gen)) | selectors resolver | a custom stylesheet |
Before - immediately
import { className, attribute, classNames } from 'effcss';
// CSS is written into the shared stylesheet right here
const centerCls = className({ margin: 'auto' });
const markerAttr = attribute({ fontWeight: 'bold' });
const utils = classNames<Utils>((selectors) => {
const { w } = selectors;
return {
[w.s]: { width: '12px' },
[w.m]: { width: '16px' }
};
});Every call produces a rule immediately, even if the result is never used on the page.
After — controlled lazy
import { lazyClassName, lazyAttribute, lazyClassNames } from 'effcss';
// nothing is generated at creation time
const centerCls = className.lazy({ margin: 'auto' });
const markerAttr = attribute.lazy({ fontWeight: 'bold' });
const utils = classNames.lazy<Utils>((selectors) => {
const { w } = selectors;
return {
[w.s]: { width: '12px' },
[w.m]: { width: '16px' }
};
});
// ... later, where the value is actually consumed:
<div className={centerCls()} {...markerAttr()}> // generates the anonymous rules
<div className={utils({ w: 'm' })} /> // generates the stylesheetNote that all lazy handlers even lazyClassName (className.lazy) and lazyAttribute (attribute.lazy) return functions. For individual rule lazy resolvers, this brings an additional bonus - casting such a resolver to a string returns the correct selector, which can be used in other styles:
const divider = className.lazy({
width: '100%',
height: '1px'
});
const layout = classNames.lazy<Layout>((selectors) => ({
[selectors.row]: {
display: 'flex'
},
// `divider` coerce to its real `.class` selector
// when `layout` will be used at the first time
[`& ${divider}`]: {
background: 'red'
}
}));Moreover, these functions can accept not only an object, but also a generator:
const divider = className.lazy(() => {
// you can create other global rules inside
// so that they are created the first time className is used
const width = variable('100%');
return {
width: width(),
height: '1px',
'&:hover': {
[width]: '50%'
}
};
});Important:
className/attributereturns a non-function value (string/ object) — they are not lazy and are not involved here.
2. Library-level — configure({ lazy: true })
configure({ lazy: true }) switches, on the fly, every generating utility that returns a function-resolver to its lazy version:
variable, variables, animation, animations, layer, layers, font, fonts, classNames, attributes, customStyles.
You keep writing the same API — no need to reach for lazy* names. Each of these utilities now defers generation until its result is used.
className and attribute are not affected: they return plain values (string / object) rather than resolvers, so they keep generating immediately.
Before — default (eager)
import { variable, animation, classNames } from 'effcss';
const shadowColor = variable('#58666d');
const spin = animation({ to: { transform: 'rotate(360deg)' } });
const utils = classNames<Utils>((selectors) => {
const { w } = selectors;
return {
[w.s]: { width: '12px' }
};
});After — configure({ lazy: true })
import { configure, variable, animation, classNames } from 'effcss';
configure({ lazy: true });
// same API, different timing
const shadowColor = variable('#58666d');
const spin = animation({ to: { transform: 'rotate(360deg)' } });
// nothing is written yet
const utils = classNames.lazy<Utils>((selectors) => {
const { w } = selectors;
return {
[w.s]: { width: '12px' }
};
});
// nothing is written yet
// CSS is produced only on first real use:
`${shadowColor()} or ${shadowColor}`
`${spin()} or ${spin}`
utils({ w: 's' })For plural utilities (variables, animations, layers, fonts, ...) the whole batch is warmed together: touching any single resolver generates the entire set, keeping ordering deterministic for SSR hydration.
Behavior notes
- Warm-up triggers — a resolver is warmed by calling it or by string coercion. Merely holding it (or calling
.get()/.set()for variables) does not generate any CSS. variable().get()/.set()— before warming,get()returns''andset()is a no-op; they become active only after the resolver has been warmed.update()— in lazy mode doesn't change variables that are not created yet.- SSR hydration — generation order is deterministic (shared internal counters), so server and client produce identical selectors as long as they warm resolvers in the same order.
subscribeevents —EffCSSEvents are emitted at the moment of actual generation (resolver warm-up), not when the utility is initially called.
Choosing between the two
- Use controlled
lazy*utilities when you want laziness only for specific rules and keep the rest eager, or when you want to guarantee laziness independently of the app-wide setting. - Use
configure({ lazy: true })when you want a broad only-what-is-used behavior across the whole library without changing your code, e.g. combined withvite-plugin-effcssto ship only the CSS that is actually rendered.