Component Structure & Variants
Component Structure & Variants
CSS describes state — it doesn't manage it. Variants are expressed through Ruby-set data-* attributes, DOM presence, and JS-toggled classes. No BEM modifier classes (--).
BEM Naming
| Level | Pattern | Example |
|---|---|---|
| Block | kebab-case noun | .panel, .media-card, .text-card |
| Element | double underscore | .panel__header, .text-card__title |
| State | single word | .open, .rotate, .loaded, .disabled |
Variants via Data Attributes
Visual variants use data-* attribute selectors, not modifier classes:
.media-card[data-media-size=large] { ... }.panel[data-bgc=primary-dark] { ... }.text-card[data-layout-style=plain-text] { ... }Attributes are set from Ruby via panel_data_opts / pattern_data_opts — never written by hand in ERB:
def panel_data_opts { bgc: layout_background_colour, panel_style: "hero" }endStructural Variants with :has()
Use :has() for conditions not known at render time:
&:has(> h1:not(:empty)) { padding-bottom: 40px; }&:has(+ .open) { transform: rotate(180deg); }Prefer data-* when the condition is known at render time. Do not use :has() to detect third-party class names — changes to external libraries will break styles silently.
State Classes
| Class | Applied to | Meaning |
|---|---|---|
.open |
Submenus, nav items, panels | Expanded/visible |
.rotate |
Arrow/chevron icons | Rotated (active) state |
.loaded |
Material Symbols icons | Icon font loaded |
.disabled |
Buttons | Non-interactive |