EffCSS v5: Self-Confident CSS-in-TS for Modern Frontend

typescript dev.to

Docs
GitHub
NPM

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:

  • variable and variables create @property rules,
  • animation and animations create @keyframes rules,
  • layer and layers create @layer rules,
  • container and containers create @container rules,
  • font and fonts creates @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:

  • stylesheet returns the created stylesheet by resolver,
  • configure affects style generation if it is called before the first stylesheet is created,
  • serialize converts 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!

Source: dev.to

arrow_back Back to Tutorials