refactor(css): give every migrated block and element a BEM base class#1079
Conversation
A BEM modifier can only exist to modify a base — but four already-migrated
components emitted lone modifier families with no base class, surfacing in the
TSX as `cva('', { variants })`:
- Accordion: no `.accordion` (only `.accordion_fill-width`, `.accordion_sidebar`)
- Alert: no `.alert__icon` (only `.alert__icon_size_*`)
- Collapsible: no `.collapsible__header` / `.collapsible__content` bases
- Link: no `.external-icon` (only `.external-icon_size_*`)
Add each base class and pass it as `cva`'s first argument. Where every modifier
shared a declaration it is hoisted onto the base (`.collapsible__header` now
carries `user-select: none`); genuinely style-free bases are emitted empty with
a `block-no-empty` disable. Link also applied its base as a bare string literal
(`cn('external-icon', …)`) — an unscoped global that matched nothing — now fixed
to the scoped `styles['external-icon']`.
DOM class strings gain the (mostly empty) base classes but rendering is
unchanged: all 80 affected visual-regression snapshots pass byte-for-byte in
both themes.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 85ebe6c The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Codify the rule that surfaced in review: a modifier must have a base to
modify, so never emit `cva('', { variants })` or a lone modifier family. Pass
the base as cva's first argument, hoist shared declarations onto it, and emit
genuinely style-free bases empty with a block-no-empty disable. Also warn
against applying a base as a bare string literal instead of the scoped
`styles[...]` name. Adds a pitfall entry and a checklist item.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Per review: the empty-base comments described things that change (which modifiers a base serves, "carries no own styles"). Collapse each to a single `block-no-empty` disable whose reason just names it as the base (`-- base block/element required by BEM`), and drop the explanatory comment on `.collapsible__header` entirely (it isn't empty — it carries `user-select`). Update the skill example to match. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…cal block Link rendered its Icon with an invented `external-icon` class — a one-off local block for a child component, which isn't valid BEM. The Icon is nested inside `<a class="link">`, so it's an element of the link block: rename the class (and its size modifiers) to `link__icon`, and rename the cva to `iconVariants`. The class is CSS-module-scoped, so only the hashed name changes — all 24 Link visual snapshots pass byte-for-byte. Deeper structural work (Icon carrying its own `.icon` block class, the wrapper span) is tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks for the review! Addressed all the comments (an AI wrote this so sorry for the gumption): Comment style (
The deeper structural work — Icon carrying its own |
Storybook Preview Deployed✅ Preview URL: https://click-4ed9d2115-clickhouse.vercel.app Built from commit: |
What
Four already-migrated components emitted a family of BEM modifier classes with no base class for them to modify — which surfaced in the TSX as the tell-tale
cva('', { variants }):.accordion.accordion_fill-width,.accordion_sidebar.alert__icon.alert__icon_size_*.collapsible__header,.collapsible__content…_indicator-dir_*.external-icon.external-icon_size_*As raised in review: "BEM model can't exist without the base class … for a modifier to exist there should be something to modify."
How
cva's first argument (cva(styles.block, { … }))..collapsible__headernow carriesuser-select: none, and the modifiers keep only their differingmargin-left.stylelint-disable-next-line block-no-emptyand a comment naming them as the base (stylelint-config-standardrejects empty rules).cn('external-icon', …)— an unscoped global class that matched nothing in the CSS Module. Now scoped viastyles['external-icon'].Verification
DOM class strings gain the (mostly empty) base classes, but rendering is unchanged:
yarn lint:css,eslint,tsc --noEmit, and unit tests (8) all green.🤖 Generated with Claude Code
Note
Low Risk
Styling-structure-only refactor across four UI components; DOM class lists gain mostly empty bases but rendering is intended to stay byte-for-byte the same.
Overview
Adds BEM base classes to Accordion, Alert, Collapsible, and Link so modifier-only
cva('', { variants })patterns are replaced with proper block/element bases wired throughcva(styles.base, …).Accordion gets an empty
.accordionbase androotVariantsforfillWidthinstead of conditionally applying only the modifier class on the root. Alert adds.alert__iconas the icon size variant base. Collapsible introduces.collapsible__header(withuser-select: nonehoisted off the indicator-dir modifiers) and an empty.collapsible__contentbase. Link renames external icon classes to.link__icon/.link__icon_size_*, scopes the icon viastyles['link__icon']instead of the broken globalcn('external-icon', …), and renamesexternalIconVariantstoiconVariants.The CSS Modules migration skill and a patch changeset document the rule: every block/element needs a base (empty +
block-no-emptydisable when needed). No intended visual or behavioral change.Reviewed by Cursor Bugbot for commit 85ebe6c. Bugbot is set up for automated code reviews on this repo. Configure here.