Skip to content

refactor(css): give every migrated block and element a BEM base class#1079

Merged
DreaminDani merged 4 commits into
mainfrom
fix/bem-base-classes
Jun 10, 2026
Merged

refactor(css): give every migrated block and element a BEM base class#1079
DreaminDani merged 4 commits into
mainfrom
fix/bem-base-classes

Conversation

@DreaminDani

@DreaminDani DreaminDani commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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 }):

Component Missing base Existing modifiers
Accordion .accordion .accordion_fill-width, .accordion_sidebar
Alert .alert__icon .alert__icon_size_*
Collapsible .collapsible__header, .collapsible__content …_indicator-dir_*
Link .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

  • Add each base class and pass it as cva's first argument (cva(styles.block, { … })).
  • Hoist shared declarations onto the base where every modifier shared one — .collapsible__header now carries user-select: none, and the modifiers keep only their differing margin-left.
  • Genuinely style-free bases are emitted empty with a stylelint-disable-next-line block-no-empty and a comment naming them as the base (stylelint-config-standard rejects empty rules).
  • Link bonus fix: it applied its base as a bare string literal — cn('external-icon', …) — an unscoped global class that matched nothing in the CSS Module. Now scoped via styles['external-icon'].

Verification

DOM class strings gain the (mostly empty) base classes, but rendering is unchanged:

  • ✅ All 80 affected visual-regression snapshots pass byte-for-byte (Accordion, Alert, Link, Collapsible), light + dark themes, zero regenerations.
  • 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 through cva(styles.base, …).

Accordion gets an empty .accordion base and rootVariants for fillWidth instead of conditionally applying only the modifier class on the root. Alert adds .alert__icon as the icon size variant base. Collapsible introduces .collapsible__header (with user-select: none hoisted off the indicator-dir modifiers) and an empty .collapsible__content base. Link renames external icon classes to .link__icon / .link__icon_size_*, scopes the icon via styles['link__icon'] instead of the broken global cn('external-icon', …), and renames externalIconVariants to iconVariants.

The CSS Modules migration skill and a patch changeset document the rule: every block/element needs a base (empty + block-no-empty disable 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.

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-bot

changeset-bot Bot commented Jun 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 85ebe6c

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@clickhouse/click-ui Patch

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

@DreaminDani DreaminDani requested a review from ariser June 10, 2026 16:50
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>
Comment thread src/components/Collapsible/Collapsible.module.css Outdated
Comment thread src/components/Accordion/Accordion.module.css Outdated
Comment thread src/components/Alert/Alert.module.css Outdated
Comment thread src/components/Collapsible/Collapsible.module.css Outdated
Comment thread src/components/Link/Link.module.css Outdated
Comment thread .claude/skills/component-css-modules-migration/SKILL.md Outdated
DreaminDani and others added 2 commits June 10, 2026 11:43
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>
@DreaminDani

DreaminDani commented Jun 10, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review! Addressed all the comments (an AI wrote this so sorry for the gumption):

Comment style (0706340f)

  • Collapsed every empty-base comment to a single line that states only the stable reason: /* stylelint-disable-next-line block-no-empty -- base block/element required by BEM */. No more mentioning specific modifiers or "carries no own styles".
  • Dropped the explanatory comment on .collapsible__header entirely — it isn't empty (it carries the hoisted user-select: none), so it stands on its own.
  • Updated the skill example to match, with a note to keep the reason stable.

external-iconlink__icon (85ebe6c5)

  • Your point about Icon being an element of the link block rather than a local block was too good to leave — renamed external-icon to the link__icon element (the Icon is nested inside <a class="link">). Scoped class, so all 24 Link visual snapshots still pass byte-for-byte.

The deeper structural work — Icon carrying its own .icon block class alongside link__icon, the redundant wrapper span, collapsing the 4 size modifiers to the 2 real icon dimensions, and auditing the other migrated components — is captured in a follow-up cleanup ticket, kept separate from the migration as we agreed.

@workflow-authentication-public

Copy link
Copy Markdown
Contributor

Storybook Preview Deployed

✅ Preview URL: https://click-4ed9d2115-clickhouse.vercel.app

Built from commit: 23e379dec80ee69f864f83f4cbc4480e711f29e8

@DreaminDani DreaminDani merged commit d732157 into main Jun 10, 2026
10 checks passed
@DreaminDani DreaminDani deleted the fix/bem-base-classes branch June 10, 2026 20:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants