Skip to content

A flexible and fully typed utility library for managing BEM-style class names in React, with support for modifiers, compound modifiers, CSS Modules, and automatic class merging.

Notifications You must be signed in to change notification settings

corvallo/bem-forge

Repository files navigation

πŸ”§ @corvallo/bem-forge

npm version npm downloads CI

A flexible and fully typed utility library for managing BEM-style class names in React, with support for modifiers, compound modifiers, CSS Modules, and automatic class merging. Why bem-forge?

  • Fully typed BEM blocks & modifiers
  • Zero string concatenation
  • Works with CSS Modules
  • Prevents invalid class names at compile time

✨ Features

  • βœ… Single config factory (bem({...})) for blocks + elements
  • βœ… Fully typed modifier system (ModifierProps, ModifierTypes)
  • βœ… Support for compound modifiers
  • βœ… bem.bind for seamless integration with CSS Modules
  • βœ… Automatic class merging via clsx
  • βœ… Optional modifier formatting (--value or --key-value)

πŸš€ Installation

pnpm add @corvallo/bem-forge
# or
npm install @corvallo/bem-forge
# or
yarn add @corvallo/bem-forge

πŸ“¦ Quick Overview

import { bem, type ModifierTypes } from "@corvallo/bem-forge";
import styles from "./Button.module.scss";

const button = bem({
  block: "button",
  modifiers: {
    size: ["sm", "md", "lg"],
    variant: ["primary", "secondary"],
    fullWidth: [true, false],
  },
  defaultModifiers: { size: "md" },
  compoundModifiers: [{ modifiers: { fullWidth: true }, class: "button--full-width" }],
  elements: {
    icon: {
      modifiers: {
        side: ["left", "right"],
      },
    },
  },
});

export const buttonClasses = bem.bind(styles, button);
export type ButtonVariants = ModifierTypes<typeof button>;

Then consume it inside React:

type ButtonProps = {
  size?: ButtonVariants["block"]["size"];
  variant?: ButtonVariants["block"]["variant"];
  iconSide?: ButtonVariants["elements"]["icon"]["side"];
} & React.ButtonHTMLAttributes<HTMLButtonElement>;

export const Button = ({ size, variant, iconSide, className, children, ...rest }: ButtonProps) => (
  <button className={buttonClasses.block({ size, variant }, className)} {...rest}>
    <span className={buttonClasses.elements.icon({ side: iconSide })} aria-hidden />
    {children}
  </button>
);

Every builder accepts a second argument, so extra className values are merged via clsx.


🧱 API

bem(options)

Creates a block factory (and optional element factories) from a single config.

const card = bem({
  block: "card",
  modifiers: { size: ["sm", "lg"] },
  elements: {
    header: { modifiers: { align: ["left", "center"] } },
  },
});

Call card.block(props, extras?) and card.elements.header(props, extras?) to build class strings.

bem.bind(styles, factory)

Transforms an entire factory into CSS Module aware helpers.

const cardClasses = bem.bind(styles, card);

cardClasses.block({ size: "sm" }, "custom"); // β†’ "_card_x _card--sm_x custom"
cardClasses.elements.header({ align: "left" });

βœ… Modifier Options

In BEM only elements use __ (e.g. block__element), while modifiers are always appended with --.... The modifierFormat option just decides whether you emit --value or --key-value.

Option Description
modifiers A list of modifier keys with possible values
defaultModifiers Default values applied when no modifier is passed
compoundModifiers Apply custom class(es) when specific modifier values match
modifierFormat Controls how modifier classes are suffixed (--value vs --key-value)
const modal = bem({
  block: "modal",
  modifiers: {
    size: ["sm", "lg"], // ← `modifiers`
  },
  defaultModifiers: {
    size: "sm", // ← `defaultModifiers`
  },
  compoundModifiers: [
    {
      modifiers: { size: "lg" },
      class: "modal--emphasis",
    },
  ], // ← `compoundModifiers`
});

modal.block({ size: "lg" }); // default => "modal modal--lg"

const modalKeyValue = bem({
  block: "modal",
  modifiers: { size: ["sm", "lg"] },
  modifierFormat: "key-value",
});

modalKeyValue.block({ size: "lg" }); // => "modal modal--size-lg"

// elements follow the same rule: base with "__", modifier with "--"
const footer = bem({
  block: "modal",
  elements: {
    footer: { modifiers: { align: ["start", "end"] }, modifierFormat: "key-value" },
  },
});

footer.elements.footer({ align: "end" }); // "modal__footer modal__footer--align-end"

🧩 Compound Modifiers

compoundModifiers: [
  {
    modifiers: { variant: "primary", size: "lg" },
    class: "button--highlight",
  },
];

🎨 CSS Modules Integration

bem.bind(styles, factory) maps every class generated by the factory to its CSS Module token, so you can keep working with clean names while React receives the hashed version. No more manual styles[className] lookups.


🧠 Typing Utility

ModifierTypes<typeof factory> returns the modifier prop types for blocks and elements:

type ButtonVariants = ModifierTypes<typeof button>;

const size: ButtonVariants["block"]["size"]; // "sm" | "md" | "lg"
const iconSide: ButtonVariants["elements"]["icon"]["side"]; // "left" | "right"

πŸ“ Folder Structure Recommendation

src/
β”œβ”€β”€ components/
β”‚   └── Button/
β”‚       β”œβ”€β”€ Button.tsx
β”‚       β”œβ”€β”€ Button.module.scss
β”‚       └── button.variants.ts

About

A flexible and fully typed utility library for managing BEM-style class names in React, with support for modifiers, compound modifiers, CSS Modules, and automatic class merging.

Resources

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •