typed-bem
Version:
A TypeScript library for generating BEM class names.
358 lines (276 loc) • 10.4 kB
Markdown
# Typed BEM
[](https://www.npmjs.com/package/typed-bem)
[](https://github.com/deleonio/typed-bem-class-generator/blob/main/LICENSE)
[](https://www.npmjs.com/package/typed-bem)
[](https://github.com/deleonio/typed-bem-class-generator/issues)
[](https://github.com/deleonio/typed-bem-class-generator/pulls)
[](https://bundlephobia.com/result?p=typed-bem)

## Overview
**Typed BEM** extends the battle-tested [easy-bem](https://www.npmjs.com/package/easy-bem) utility with first-class TypeScript
support. You describe the valid blocks, elements, and modifiers once, and Typed BEM turns that definition into:
- type-safe class name factories,
- helper utilities for composing class lists, and
- an optional SCSS file generator that mirrors your schema.
By centralizing your BEM schema in TypeScript you remove guesswork, prevent typos, and keep styles synchronized across your
project.
## Reference Project
This library is used in the [KoliBri](https://github.com/public-ui/kolibri) project - an accessible web component library that provides a comprehensive set of UI components following accessibility best practices. KoliBri demonstrates real-world usage of Typed BEM for maintaining consistent and type-safe BEM class names across a large component library.
## Key Features
- **Type-Safe API** – Catch invalid blocks, elements, or modifiers during development instead of at runtime.
- **Set-Based Modifiers** – Model allowed modifiers with `Set<string>` for fast lookups and precise literal unions.
- **SCSS Generator** – Produce starter SCSS files that follow the same structure as your TypeScript schema.
- **Tree-Shakeable Helpers** – `generateBemClassNames`, `uniqueClassNames`, and `generateBemScssFile` are exported individually.
- **Lightweight Footprint** – Built directly on top of `easy-bem` with no extra runtime dependencies.
## Installation
Install the package with your preferred package manager:
```bash
npm install typed-bem
# or
pnpm add typed-bem
yarn add typed-bem
```
## Quick Start
### 1. Describe your schemas
Define a TypeScript type that captures the blocks, their elements, and the allowed modifiers. Use `Set` when modifiers are
allowed and `null` when they are not. **Best Practice:** Define all your components in a central schema for better maintainability.
```typescript
import { generateBemClassNames } from 'typed-bem';
// Central schemas for all components (recommended approach)
type ComponentsSchema = {
button: {
modifiers: Set<'primary' | 'secondary'> | null;
elements: {
icon: {
modifiers: Set<'small' | 'large'> | null;
};
text: {
modifiers: null;
};
};
};
input: {
modifiers: Set<'error' | 'success'> | null;
elements: {
label: {
modifiers: Set<'required' | 'disabled'> | null;
};
field: {
modifiers: null;
};
};
};
};
```
### 2. Generate class names
`generateBemClassNames` reads your schemas and returns a strongly typed helper. Invalid combinations immediately trigger
TypeScript errors.
```typescript
// Create a single BEM instance for all components
const bem = generateBemClassNames<ComponentsSchema>();
// Direct usage (always works)
bem('button');
// "button"
bem('button', { primary: true });
// "button button--primary"
bem('button', 'icon', { small: true });
// "button__icon button__icon--small"
// bem('button', { tertiary: true }); // TypeScript error: unknown modifier
// bem('button', 'label'); // TypeScript error: unknown element
```
### 3. Alternative Short Syntax
For components where you work primarily with one block or element, you can use the `forBlock()` and `forElement()` methods. These are optional shortcuts for convenience.
#### Block-bound generators
```typescript
// Create block-specific generators
const buttonBem = bem.forBlock('button');
const inputBem = bem.forBlock('input');
// Now you can omit the block name!
buttonBem(); // "button"
buttonBem({ primary: true }); // "button button--primary"
buttonBem('icon', { small: true }); // "button__icon button__icon--small"
inputBem({ error: true }); // "input input--error"
inputBem('label', { required: true }); // "input__label input__label--required"
```
#### Element-bound generators
```typescript
// Create element-specific generators
const buttonIconBem = buttonBem.forElement('icon');
const inputLabelBem = inputBem.forElement('label');
// Ultra-short syntax for common elements
buttonIconBem(); // "button__icon"
buttonIconBem({ small: true }); // "button__icon button__icon--small"
inputLabelBem({ required: true }); // "input__label input__label--required"
```
These methods are particularly useful for repetitive tasks but are not required for general usage.
### 4. Merge class names
Use `uniqueClassNames` to combine dynamic class name fragments while removing duplicates and falsy values.
```typescript
import { uniqueClassNames } from 'typed-bem';
const className = uniqueClassNames(bem('button'), bem('button', 'icon', { small: props.isSmall }), props.className);
```
### 5. Generate a SCSS skeleton (optional)
Typed BEM can mirror your schema into an SCSS file. When creating `Set` values for modifiers, cast array literals with `as const`
so the literal types stay intact.
```typescript
import { generateBemScssFile } from 'typed-bem/scss';
const componentDefinition: ComponentsSchema = {
button: {
modifiers: new Set(['primary', 'secondary'] as const),
elements: {
icon: { modifiers: new Set(['small', 'large'] as const) },
text: { modifiers: null },
},
},
input: {
modifiers: new Set(['error', 'success'] as const),
elements: {
label: { modifiers: new Set(['required', 'disabled'] as const) },
field: { modifiers: null },
},
},
};
generateBemScssFile(componentDefinition, './components');
```
The generator writes a file named `components.scss` next to your script:
```scss
.button {
&--primary {
// Styles for button--primary
}
&--secondary {
// Styles for button--secondary
}
&__icon {
&--small {
// Styles for button__icon--small
}
&--large {
// Styles for button__icon--large
}
}
&__text {
// Styles for button__text
}
}
.input {
&--error {
// Styles for input--error
}
&--success {
// Styles for input--success
}
&__label {
&--required {
// Styles for input__label--required
}
&--disabled {
// Styles for input__label--disabled
}
}
&__field {
// Styles for input__field
}
}
```
## Usage Patterns
### Central Schema Registration (Recommended)
Instead of creating separate `generateBemClassNames` instances for each component, register all components in a central schema:
```typescript
// schema.ts - Central BEM schema
type AppBemSchema = {
header: {
/* ... */
};
navigation: {
/* ... */
};
button: {
/* ... */
};
input: {
/* ... */
};
modal: {
/* ... */
};
// ... all your components
};
export const bem = generateBemClassNames<AppBemSchema>();
```
```typescript
// components/Button.tsx
import { bem } from '../schema';
const buttonBem = bem.forBlock('button');
export function Button({ variant, size, children }) {
return (
<button className={buttonBem({ [variant]: true })}>
{children}
</button>
);
}
```
### Migration from Direct Usage
The new API is fully backward compatible. You can migrate incrementally:
```typescript
// Before (still works)
const oldStyle = bem('button', { primary: true });
// After (more convenient for single-block components)
const buttonBem = bem.forBlock('button');
const newStyle = buttonBem({ primary: true });
```
### Component-Specific Patterns
```typescript
// For components with frequent element usage
const modalBem = bem.forBlock('modal');
const modalHeaderBem = modalBem.forElement('header');
const modalBodyBem = modalBem.forElement('body');
const modalFooterBem = modalBem.forElement('footer');
// Usage in component
<div className={modalBem({ open: isOpen })}>
<header className={modalHeaderBem()}>Title</header>
<main className={modalBodyBem()}>Content</main>
<footer className={modalFooterBem()}>Actions</footer>
</div>
```
## API Reference
### `generateBemClassNames`
```typescript
declare function generateBemClassNames<B extends BemBlocks<BemSchema>>(): TypedBemFunction<B>;
```
- **Returns** a cached `bem` function with additional methods.
- **Parameters**
- `blockName` – a key from your schema.
- `blockModifiersOrElementName` – either a partial record of block modifiers or an element name.
- `elementModifiers` – (optional) a partial record of element modifiers when targeting an element.
#### Extended Methods
- **`.forBlock(blockName)`** – Returns a block-bound generator that doesn't require the block name.
- **`.forElement(elementName)`** – Available on block-bound generators, returns an element-bound generator.
**Example:**
```typescript
const bem = generateBemClassNames<Schema>();
// Direct usage
bem('button', { primary: true });
// Block-bound usage
const buttonBem = bem.forBlock('button');
buttonBem({ primary: true });
// Element-bound usage
const iconBem = buttonBem.forElement('icon');
iconBem({ small: true });
```
### `uniqueClassNames`
```typescript
declare function uniqueClassNames(...chunks: (string | undefined | null | false)[]): string;
```
- Flattens the provided arguments into a single class string.
- Ignores falsy values and empty strings.
- Removes duplicate class names in the final result.
### `generateBemScssFile`
```typescript
declare function generateBemScssFile<B extends BemBlocks<BemSchema>>(definition: B, outputPath: string): void;
```
- Writes `<outputPath>.scss` in the current working directory.
- Accepts the same schema object you use to generate class names.
- Intended for Node.js environments; import it from `typed-bem/scss`.
## License
Typed BEM is distributed under the [MIT License](https://github.com/deleonio/typed-bem-class-generator/blob/main/LICENSE).