Runtime-agnostic typed BEM class generation framework.
Elemia wraps your existing CSS Modules (or generates scoped classes from scratch) and exposes a fully type-safe, autocomplete-friendly API for building and applying BEM class names. It works in any framework — React, Vue, Svelte, SolidJS, or plain JavaScript — and integrates with Vite and ESLint for a complete developer experience.
| Package | Version | Description |
|---|---|---|
@elemia/core |
0.1.0 | Core block() factory and type system |
@elemia/styles |
0.1.0 | CSS-in-JS stylesheet authoring factory |
@elemia/react |
0.1.0 | React adapter — useBlock, StyleProvider, SSR |
@elemia/vue |
0.1.0 | Vue 3 composable adapter — useBlock, StyleProvider |
@elemia/svelte |
0.1.0 | Svelte action adapter — styleAction |
@elemia/solid |
0.1.0 | SolidJS reactive primitive — createBlock |
@elemia/vanilla |
0.1.0 | Vanilla JS style injection — mount |
@elemia/plugin-vite |
0.1.0 | Vite plugin — path injection, CSS extraction, dev overlay |
@elemia/cli |
0.1.0 | CLI — schema inference, validation, migration tooling |
@elemia/eslint-plugin |
0.1.0 | ESLint rules for migration and usage enforcement |
BEM is a proven CSS naming methodology, but applying it manually is error-prone and produces untyped, stringly-typed class name logic scattered throughout components. Elemia solves this by:
# Core only (wrapper mode)
pnpm add @elemia/core
# With a framework adapter
pnpm add @elemia/core @elemia/styles @elemia/react
# Build plugin (add to devDependencies)
pnpm add -D @elemia/plugin-vite
No CSS changes required. Elemia wraps your existing module and adds type safety.
// card.ts
import styles from './Card.module.css'
import { block } from '@elemia/core'
const b = block(styles, {
elements: ['title', 'body', 'footer'] as const,
modifiers: {
size: ['sm', 'md', 'lg'] as const,
disabled: true,
},
})
// Usage
b.root() // → 'card'
b('title') // → 'card__title'
b(null, { size: 'lg' }) // → 'card card--size-lg'
b('body', { disabled: true }) // → 'card__body card__body--disabled'
b.has('title') // → true (runtime element guard)
Define your CSS inside styles() alongside your schema, then reference it with block().
import { block } from '@elemia/core'
import { styles } from '@elemia/styles'
const sheet = styles({
root: { display: 'flex', gap: '1rem' },
title: { fontSize: '1.25rem', fontWeight: 700 },
}, { blockName: 'card' })
const b = block('card', {
__filePath: import.meta.url,
elements: ['title'] as const,
modifiers: { size: ['sm', 'md', 'lg'] as const },
})
Add @elemia/plugin-vite to extract CSS at build time.
// vite.config.ts
import { defineConfig } from 'vite'
import { elemia } from '@elemia/plugin-vite'
export default defineConfig({
plugins: [elemia()],
})
The plugin injects __filePath automatically and extracts authored styles to static CSS.
import { useBlock } from '@elemia/react'
function Card({ size }: { size: 'sm' | 'md' | 'lg' }) {
const b = useBlock(sheet)
return <div className={b(null, { size })}>{/* ... */}</div>
}
For SSR, wrap your app with StyleProvider and render ServerStyles in your document head.
import { StyleProvider, ServerStyles } from '@elemia/react'
// In your SSR entry:
<StyleProvider>
<App />
</StyleProvider>
// In your document <head>:
<ServerStyles />
import { useBlock } from '@elemia/vue'
const b = useBlock(sheet)
<script>
import { styleAction } from '@elemia/svelte'
</script>
<div use:styleAction={sheet}>...</div>
import { createBlock } from '@elemia/solid'
const b = createBlock(sheet)
import { mount } from '@elemia/vanilla'
const unmount = mount(sheet, { target: document.head })
// Call unmount() to remove styles when done
Elemia supports four modifier types:
const b = block('card', {
modifiers: {
// Boolean: applies '--disabled' or nothing
disabled: true,
// Enum: applies '--size-lg' etc.
size: ['sm', 'md', 'lg'] as const,
// Multi-select: accepts single string or array
tags: { values: ['a', 'b', 'c'] as const, multi: true },
// Custom map: full control
theme: { map: (v) => v.toLowerCase() },
},
})
b(null, { disabled: true }) // → 'card card--disabled'
b(null, { size: 'lg' }) // → 'card card--size-lg'
b(null, { tags: ['a', 'b'] }) // → 'card card--tags-a card--tags-b'
Modifiers are always output in alphabetical key order for deterministic class strings.
Elements can be declared as an array or a record for fine-grained control:
const b = block(styles, {
// Array form (no element-level modifiers)
elements: ['title', 'body'] as const,
// Record form (with per-element modifiers and inheritance)
elements: {
title: {
modifiers: { truncated: true },
inherit: ['disabled'], // inherit block-level 'disabled' modifier
},
},
})
# Infer a typed schema from a CSS file
elemia generate-types src/Card.module.css
# Validate schema against CSS in CI
elemia check src/ --strict
# Generate a JSON manifest of all blocks
elemia manifest src/
# Compare generated output against original CSS
elemia compare src/Card.module.css
# Convert CSS to styles() format
elemia convert src/Card.module.css
// eslint.config.js
import elemia from '@elemia/eslint-plugin'
export default [
elemia.configs.recommended,
]
Rules:
elemia/no-template-classnames — flags template literals joining class names, suggests block() migrationelemia/consistent-block-usage — enforces all-wrapper or all-raw consistency per file (opt-in)Author-mode blocks are scoped deterministically using a djb2a hash:
hash input: salt::normalizedFilePath::blockName
hash output: blockName_abc123 (6 chars, base-36)
The Vite plugin injects the file path at build time. In development without the plugin, Elemia falls back to a name-only hash and logs a console warning.
| Mode | Size (min+gzip) |
|---|---|
| Wrapper mode only | ≤ 2 KB |
| Core (both modes) | ≤ 3 KB |
MIT