Skip to content

Instantly share code, notes, and snippets.

@snigo
Created February 4, 2026 14:01
Show Gist options
  • Select an option

  • Save snigo/ed2a53c832132080bc4e7dbdb0190bff to your computer and use it in GitHub Desktop.

Select an option

Save snigo/ed2a53c832132080bc4e7dbdb0190bff to your computer and use it in GitHub Desktop.
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.

Design System Styling

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.


1. Ship styles per component

  • Co-locate one CSS file per component (e.g. Button.css next to Button.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.

2. Use native CSS

  • 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).

3. Prefer CSS classes over attributes for styling

  • 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.

4. Global CSS, not CSS Modules

  • 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.

5. BEM: blocks, elements, modifiers

  • 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, not Button--lg. The key (size) must be explicit so lg is unambiguous and maps clearly to the component prop. Prevents semantic collisions across axes (e.g. size vs density).

6. Modifiers set variables; blocks/elements consume them

  • 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: …;
}

7. Pseudo-classes are not modifiers

  • 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).


8. Cascade layers for overridable defaults

  • 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;

9. Bootstrap / scaffolding

  • 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.
  • Ensure the generated CSS template includes the layer boilerplate by default.

10. Tailwind and CSS-in-JS

  • 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.

Quick checklist

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment