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

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:

  • 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 create @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 = {
  [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:

  • 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!

Comments

No comments yet. Start the discussion.