Skip to content

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:

FlavorTriggerScope
Controlledopt-in lazy* utilitiesper-utility, always lazy
Library-levelconfigure({ 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() (or fn(...args)),
  • coerce it to a string — `${fn}` or String(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.

UtilityReturnsGenerates
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 resolvera class-selector stylesheet
lazyAttributes(gen) (or attributes.lazy(gen))selectors resolveran attribute-selector stylesheet
lazyCustomStyles(gen) (or customStyles.lazy(gen))selectors resolvera custom stylesheet

Before - immediately

ts
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

ts
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 stylesheet

Note 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:

ts
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:

ts
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 / attribute returns 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)

ts
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 })

ts
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 '' and set() 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.
  • subscribe eventsEffCSSEvents 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 with vite-plugin-effcss to ship only the CSS that is actually rendered.

Released under the Apache-2.0 License.