Skip to content

Comparison ​

Here ECSS is compared with popular approaches to styling components using a single example — a button with style variants. ECSS's main difference: the logic of "which style to apply" lives in the stylesheet, not in the component, and is fully typed at the same time.

CSS Modules ​

CSS Modules solve the problem of global names, but the state logic stays in the component. Every new variant means changes in two places: the CSS file and the JS component.

css
/* Button.module.css */
.button {
  display: inline-flex;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;
}

.primary {
  background: #646cff;
  color: #fff;
}

.danger {
  background: #e53e3e;
  color: #fff;
}

.ghost {
  background: transparent;
  border: 1px solid currentColor;
}

.disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
tsx
import styles from './Button.module.css';

function Button({ variant, disabled, children }) {
  const className = [
    styles.button,
    variant === 'primary' && styles.primary,
    variant === 'danger' && styles.danger,
    variant === 'ghost' && styles.ghost,
    disabled && styles.disabled,
  ]
    .filter(Boolean)
    .join(' ');

  return <button className={className}>{children}</button>;
}

With ECSS everything is declared in a single file, and the component knows nothing about any classes:

ecss
/* Button.ecss */
@enum Variant {
  values: "primary", "danger", "ghost";
}

@block Button {
  @param --variant Variant;
  @param --disabled? boolean;

  display: inline-flex;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;

  @if (--variant == "primary") {
    background: #646cff;
    color: #fff;
  }

  @if (--variant == "danger") {
    background: #e53e3e;
    color: #fff;
  }

  @if (--variant == "ghost") {
    background: transparent;
    border: 1px solid currentColor;
  }

  @if (--disabled) {
    opacity: 0.4;
    cursor: not-allowed;
  }
}
tsx
import { EButton } from './Button.ecss';

<EButton as="button" params={{ variant: 'primary' }}>
  Button
</EButton>;
  • No need to assemble className by hand
  • The state logic lives next to the styles
  • TypeScript knows the allowed values of variant — you can't pass a nonexistent one

styled-components ​

styled-components let you write styles directly in JS, using props for variants. However, the approach has several significant downsides: the CSS is generated at runtime on every render, the library itself adds tens of kilobytes to the bundle, and template strings with JS interpolations quickly turn into a hard-to-read mix of two languages.

tsx
import styled, { css } from 'styled-components';

const Button = styled.button<{
  variant: 'primary' | 'danger' | 'ghost';
  disabled?: boolean;
}>`
  display: inline-flex;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;

  ${({ variant }) =>
    variant === 'primary' &&
    css`
      background: #646cff;
      color: #fff;
    `}

  ${({ variant }) =>
    variant === 'danger' &&
    css`
      background: #e53e3e;
      color: #fff;
    `}

  ${({ variant }) =>
    variant === 'ghost' &&
    css`
      background: transparent;
      border: 1px solid currentColor;
    `}

  ${({ disabled }) =>
    disabled &&
    css`
      opacity: 0.4;
      cursor: not-allowed;
    `}
`;

ECSS is processed at build time — plain static CSS reaches the browser:

ecss
/* Button.ecss */
@enum Variant {
  values: "primary", "danger", "ghost";
}

@block Button {
  @param --variant Variant;
  @param --disabled? boolean;

  display: inline-flex;
  padding: 8px 16px;
  border-radius: 6px;
  cursor: pointer;

  @if (--variant == "primary") {
    background: #646cff;
    color: #fff;
  }

  @if (--variant == "danger") {
    background: #e53e3e;
    color: #fff;
  }

  @if (--variant == "ghost") {
    background: transparent;
    border: 1px solid currentColor;
  }

  @if (--disabled) {
    opacity: 0.4;
    cursor: not-allowed;
  }
}
tsx
import { EButton } from './Button.ecss';

<EButton as="button" params={{ variant: 'primary' }}>
  Button
</EButton>;
  • No runtime overhead: the CSS is generated once at build time
  • Works with SSR without additional setup
  • Styles don't mix with JS logic