Modifier conventions
cssdoc is framework-agnostic, so it doesn't assume one way of spelling a component's modifier variations. A modifier convention tells the parser how to recognize a modifier on a base class, how to render it back into a selector, and how to detect its use in HTML.
The default is BEM — a modifier is conjoined onto the base class with --, like .button--primary. Set a different convention in cssdoc.json or via ParseOptions.
Presets
Three presets ship by name:
| Preset | Structure | Example | prop / value |
|---|---|---|---|
bem | suffix | .button--primary | prop: "primary" |
rscss | chained | .button.-color-secondary | prop: "color", value: "secondary" |
bare | chained | .button.primary | prop: "primary" |
{
"$schema": "https://cssdoc.dev/cssdoc.schema.json",
"modifierConvention": "rscss",
}The three structural forms
A custom convention is an object with a structure, a separator, and optional prop/value splitting. Every well-known scheme is one of three structures:
interface ModifierConvention {
structure: "chained" | "suffix" | "attribute";
separator: string | string[]; // interpreted per structure (see below)
elementSeparator?: string | string[]; // BEM element delimiter, e.g. "__" → parts
statePrefixes?: string[]; // state-class prefixes, e.g. ["is-","has-"] → states
statePseudoClasses?: string[]; // native pseudo-classes as states, e.g. ["disabled"]; overrides the default set
propValue?: boolean; // split the body into prop/value? (default false)
propValueSeparator?: string; // default "-"
}chained— a separate class chained to the base.separatoris the required class prefix (""means any chained class).suffix— appended into the base class name.separatoris the delimiter between the base and the modifier body.attribute— an attribute selector on the base.separatoris the required attribute-name prefix (""means any attribute).
separator may be an array — any one of the values marks a modifier, useful for state prefixes like is-/has-. Values are matched literally (never as a regex):
{ "modifierConvention": { "structure": "chained", "separator": ["is-", "has-"] } }.card.is-open {
/* → modifier "is-open" */
}
.card.has-icon {
/* → modifier "has-icon" */
}Elements and states
A convention can also name two roles that aren't modifiers, so they land in the model's parts and states instead of being flattened into modifiers:
elementSeparator— a BEM-style element delimiter inside the base class name. A matching class is recorded as a part. The default BEM preset sets"__", so.card__titleis a part namedcard__title.statePrefixes— class prefixes that mark a state. A class chained to the base whose name starts with one of these is recorded as a state and is never treated as a modifier. It's opt-in — no preset sets it.statePseudoClasses— native pseudo-classes recognized as states. cssdoc ships a curated default set of form/UI states (:disabled,:checked,:open,:required,:valid, …) — deliberately not:hover/:focus— so.tab:disabledis apseudo-classstate. Set this to override the default. (Custom:state(x)states and authored@cssstate :xare always captured regardless.)
{
"modifierConvention": {
"structure": "suffix",
"separator": "--",
"elementSeparator": "__",
"statePrefixes": ["is-", "has-"],
},
}.card__title {
/* → part "card__title" */
}
.card--featured {
/* → modifier "card--featured" */
}
.card.is-loading {
/* → state "is-loading" (not a modifier) */
}An element's own modifiers attach to the part: .card__title--active gives the part card__title a modifier active (in its modifiers), rather than inventing a part named card__title--active. Consumer-side usage is checked too — a class that looks like a state (statePrefixes) or an element (elementSeparator) but isn't documented raises unknown-state / unknown-part.
Name-case conventions
Enforce a case on component and part class names — e.g. SUIT's PascalCase components — with the naming block. Each entry is a preset (pascalCase, camelCase, lowercase) or a custom regular expression tested against the class name. The *-name-case rules only fire when naming is set.
{
"modifierConvention": "bem",
"naming": {
"component": "pascalCase", // SUIT: .Card, .SiteHeader
"part": "camelCase",
},
}| Preset | Pattern | Matches |
|---|---|---|
pascalCase | ^[A-Z][A-Za-z0-9]*$ | Card, SiteNav |
camelCase | ^[a-z][A-Za-z0-9]*$ | card, siteNav |
lowercase | ^[a-z][a-z0-9-]*$ | card, site-nav |
A custom value is used as a regex source (e.g. "^c-[a-z]"). It's your own config and is only tested against short class names, but a pathological pattern can still be slow — prefer a preset where one fits.
In code
The convention is also settable programmatically:
import { parseCssDocs } from "@cssdoc/core";
const model = parseCssDocs(css, { modifierConvention: "rscss" });
// or a custom object:
parseCssDocs(css, { modifierConvention: { structure: "attribute", separator: "data-" } });Examples
BEM (preset, default)
// {"modifierConvention": "bem"} underlying structure
{ "modifierConvention": { "structure": "suffix", "separator": "--" } }.card {
/* … */
}
.card--featured {
/* … */
} /* → modifier "card--featured", prop "featured" */Learn more about BEM
rscss (preset)
{ "modifierConvention": "rscss" }.card.-color-primary {
/* → modifier "-color-primary", prop "color", value "primary" */
}Learn more about rscss
SUIT
Structurally identical to BEM; but requires PascalCase for components.
{
"modifierConvention": { "structure": "suffix", "separator": "--" },
"naming": { "component": "pascalCase" },
}.Card--featured {
/* → modifier "Card--featured", prop "featured" */
}Learn more about SUIT
CUBE CSS
CUBE exceptions are data attributes, which map cleanly onto prop/value.
{ "modifierConvention": { "structure": "attribute", "separator": "data-" } }.card[data-variant="ghost"] {
/* → prop "variant", value "ghost" */
}
.card[data-loading] {
/* → prop "loading" (boolean) */
}Learn more about CUBE
OOCSS
Standalone modifier classes chained to the base — the same as the bare preset. Because every chained class is a candidate, pair it with "unknown-modifier": "off" (see below) so unrelated classes aren't flagged.
{
"modifierConvention": { "structure": "chained", "separator": "" },
"rules": { "unknown-modifier": "off" },
}.card.featured {
/* → modifier "featured" */
}is- state classes
{ "modifierConvention": { "structure": "chained", "separator": "is-" } }.card.is-loading {
/* → modifier "is-loading", prop "loading" */
}