| name | design-system-styling |
|---|---|
| description | Apply design-system styling rules when authoring or refactoring component CSS in a design system or component library. Use when adding components, writing or editing component styles, choosing between classes vs attributes, using BEM, managing overrides, or discussing Tailwind/CSS-in-JS vs plain CSS in a design system. |
Styling a design system is not the same as styling an application. This skill encodes rules so component styles stay consumable, overridable, and maintainable at scale.
When to use: Adding or editing component CSS, choosing selectors (classes vs attributes), defining modifiers, managing cascade/overrides, or evaluating Tailwind/CSS-in-JS in a design system.
- Co-locate one CSS file per component (e.g.
Button.cssnext toButton.tsx). - No single global bundle for all component styles; each component imports its own CSS.
- Benefits: tree-shaking, safer refactors, easier reviews. Co-location keeps tests, docs, and styles with the component.
- Use plain CSS. Avoid Sass/LESS/SCSS unless the project explicitly requires advanced mixins or logic.
- Rely on native features: custom properties (variables), nesting,
color-mix, etc. - Plain CSS is universal and keeps the design system usable in any stack (Tailwind, CSS-in-JS, or vanilla).
- Use classes for visual styling. Do not use
data-*or ARIA for style hooks. - Why: ARIA can change for accessibility without any visual change; coupling style to ARIA or data breaks when semantics or config change. Data attributes are for configuration/metadata; classes are for styling only.
- Responsibility split:
- ARIA → accessibility semantics
- data-* → configuration / metadata
- CSS classes → visual styling (single responsibility)
- Classes also benefit from better tooling, DevTools, and selector performance.
- Do not use CSS Modules (hashed/local class names) for design system component styles.
- Design system styles are part of the public API: consumers must be able to target and override classes predictably.
- Hashed names make “styling as API” impossible. Class names should be designed and stable, not hidden.
- Use BEM-style naming so relationships and intent are clear in the class name.
- Block = component (e.g.
Button,LoadingButton). - Element = part of the block (e.g.
Button__SpinnerIcon); maps to primitives/children. - Modifier = variant from props (e.g.
Button--variant-primary,Button--size-lg).
Pattern: <block>__<element>--<modifier>.
- Key-qualified modifiers: Use
Button--size-lg, notButton--lg. The key (size) must be explicit solgis unambiguous and maps clearly to the component prop. Prevents semantic collisions across axes (e.g. size vs density).
- Modifier classes must not define layout/colors directly. They should only set CSS custom properties (e.g.
--button-bg,--button-fg,--button-palette-bg). - Block and element rules own the actual styles and consume those variables.
- This keeps modifiers composable and avoids selector explosion (e.g.
.Button--variant-primary.Button--color-destructive { … }). Prefer rule explosion (more rules, flat selectors) over selector explosion (fewer rules, combined selectors). - Why: Combined selectors increase specificity and lock in composition; design systems should keep specificity low so consumers can override without escalation.
Good:
.Button {
background-color: var(--button-bg);
color: var(--button-fg);
}
.Button--color-destructive {
--button-palette-bg: var(--color-bg-destructive);
--button-palette-fg: var(--color-fg-on-destructive);
}
.Button--variant-primary {
--button-bg: var(--button-palette-bg);
--button-fg: var(--button-palette-fg);
}Avoid:
.Button--variant-primary.Button--color-destructive {
background-color: …;
color: …;
}- Handle
:hover,:focus,:focus-visible,:disabled, etc. in block or element rules, using variables set by modifiers (e.g.--button-bg-hover). - Do not treat pseudo-classes as BEM modifiers. Modifiers represent design-system state (props); pseudo-classes represent interaction/platform state.
Good:
.Button:hover {
background-color: var(--button-bg-hover);
}Avoid: Using modifier-like selectors for hover/focus (e.g. overloading --button-bg inside :hover in a way that conflates variant and interaction).
- Put all design system component CSS inside a named layer (e.g.
@layer design-system { … }). - This gives a deterministic cascade: consumer styles (unlayered or in a later layer) consistently override design system styles without relying on import order.
- Without layers, override order depends on bundle order and can differ across apps.
In each component CSS file:
@layer design-system {
.Button { … }
.Button--size-lg { … }
}In the consuming app (if using Tailwind), declare layer order so utilities win over the design system:
@layer design-system, base, components, utilities;- Use a bootstrap or scaffolding script to generate new components so that:
- New CSS files are created with the correct
@layer design-system { }wrapper. - Engineers and automation don’t forget the layer.
- New CSS files are created with the correct
- Ensure the generated CSS template includes the layer boilerplate by default.
- Tailwind: Great for apps; for a drop-in design system that works with any stack, avoid making Tailwind a required dependency. Use plain CSS so consumers can use Tailwind, vanilla CSS, or something else. If both are used, layer ordering (see above) keeps overrides predictable.
- CSS-in-JS (Emotion, styled-components, etc.): Solves scoping and theming well for apps, but adds a runtime and ecosystem commitment. For a small, environment-agnostic library, plain CSS keeps the contract explicit and dependency-free.
- Summary: Plain CSS is “neutral” because it’s universal; it doesn’t require consumers to adopt a specific styling stack.
When adding or editing component styles:
- Styles are in a per-component CSS file, co-located with the component.
- Native CSS only (no preprocessors unless required).
- Styling is driven by classes, not
data-*or ARIA. - Class names follow BEM; modifiers are key-qualified (e.g.
--size-lg,--variant-primary). - Modifiers only set CSS variables; block/element rules consume them (no selector explosion).
- Pseudo-classes are handled in block/element rules, not as modifiers.
- All component rules are wrapped in
@layer design-system. - Bootstrap/templates generate CSS with the layer so new components are correct by default.