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
- β
Single config factory (
bem({...})) for blocks + elements - β
Fully typed modifier system (
ModifierProps,ModifierTypes) - β Support for compound modifiers
- β
bem.bindfor seamless integration with CSS Modules - β
Automatic class merging via
clsx - β
Optional modifier formatting (
--valueor--key-value)
pnpm add @corvallo/bem-forge
# or
npm install @corvallo/bem-forge
# or
yarn add @corvallo/bem-forgeimport { 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.
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.
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" });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"compoundModifiers: [
{
modifiers: { variant: "primary", size: "lg" },
class: "button--highlight",
},
];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.
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"src/
βββ components/
β βββ Button/
β βββ Button.tsx
β βββ Button.module.scss
β βββ button.variants.ts