Skip to content

Latest commit

Β 

History

History
590 lines (417 loc) Β· 20.6 KB

File metadata and controls

590 lines (417 loc) Β· 20.6 KB

CONTRIBUTOR-DOCS / Style guide / 2nd-Gen CSS / Styling Anti-Patterns (What to Avoid)

Styling Anti-Patterns (What to Avoid)

In this doc

This appendix lists common mistakes encountered when adopting the 2nd-gen SWC styling model, why they happen, and what to do instead.

Each anti-pattern is grounded in real Spectrum source patterns. Badge and Status Light are reference implementations for correct patterns.

πŸ“– Reference implementations: Badge Β· Status Light Β· Reference Migration: Badge

1. Leaving Visual Styles on :host

❌ Anti-Pattern

:host {
  padding: 8px;
  background-color: var(--spectrum-badge-background-color-default);
}

Why This Happens

  • Spectrum CSS often treated the custom element root as the primary styling surface
  • Incremental migrations make it tempting to keep existing rules in place

Why This Is a Problem

  • :host is part of the public styling API
  • Visual styles here are harder to override predictably
  • This breaks the SWC model where :host defines layout participation only

βœ… Correct Approach

:host {
  display: inline-flex;
}

.swc-Badge {
  background: var(
    --swc-badge-background-color,
    token("neutral-subdued-background-color-default")
  );
}

πŸ”Ž Badge reference:
See the migrated Badge where :host is limited to layout (display, place-self, vertical-align) and all visual styling lives on .swc-Badge.

πŸ“– See: Component CSS Style Guide β†’ Rule order

Exception: styles that must target the host element directly

Three categories of styles may legitimately live on :host, each for a distinct reason:

  1. UA style resets β€” the browser applies default styles directly to the host element (for example, the native popover stylesheet). Those cannot be overridden from an inner class and must be reset on :host.
  2. Entry/exit transitions β€” opacity, transition-*, and transition-behavior: allow-discrete must be on :host when the host element is itself the transition target (for instance, when @starting-style or overlay applies to the host rather than a descendant).
  3. Positioning surface β€” position: absolute, inset: auto, and dimension constraints belong on :host when an external controller (such as a placement controller) writes coordinates directly to the host element.

πŸ“– See: Component CSS Style Guide β†’ When to use :host

2. Preserving --mod-* as an Extra Indirection Layer

This anti-pattern reflects one of the most common and subtle migration mistakes.

❌ Anti-Pattern

Preserving Spectrum-era --mod-* fallback chains, or introducing an SWC equivalent:

min-block-size: var(--mod-badge-height, var(--swc-badge-height));
border-radius: var(--mod-badge-corner-radius, var(--swc-badge-corner-radius));
background: var(--mod-badge-background-color-default, var(--swc-badge-background-color-default));

or:

min-block-size: var(--swc-mod-badge-height, token("component-height-100"));

Why This Happens

  • --mod-* functioned as a lightweight override hook in Spectrum CSS
  • It allowed customization without modifying base rules
  • Preserving the pattern can feel safer during migration

Why This Is a Problem

  • --mod-* adds an unnecessary layer of indirection
  • Long fallback chains are harder to reason about and override
  • It obscures which values are intentionally exposed by the component
  • It conflicts with SWC’s model of explicit component-level customization

βœ… Correct Approach

  • Remove --mod-* entirely
  • Collapse the fallback chain into a single component custom property
  • Decide whether the property should be:
    • exposed (--swc-*)
    • or internal (--_swc-*)
  • Reference design tokens directly via token()
.swc-Badge {
  min-block-size: var(--swc-badge-height, token("component-height-100"));
  border-radius: var(
    --swc-badge-corner-radius,
    token("corner-radius-medium-size-medium")
  );
  background: var(
    --swc-badge-background-color,
    token("neutral-subdued-background-color-default")
  );
}

πŸ”Ž Badge reference:
See the Badge migration where all --mod-* β†’ spectrum β†’ property chains are collapsed into intentional --swc-badge-* properties.

πŸ“– See: Custom Properties Style Guide β†’ Component custom property exposure

3. Excess Variant Classes in render()

❌ Anti-Pattern

This is an anti-pattern when the class is not being used in the actual component stylesheet.

classMap({
  [`spectrum-Badge--size${this.size?.toUpperCase()}`]:
    typeof this.size !== 'undefined',
})

Why This Happens

  • Legacy Spectrum class-based patterns
  • Uncertainty about expressing variants via attributes

Why This Is a Problem

  • Duplicates logic already expressed by attributes
  • Extranneous when class not actually being used as a style hook

βœ… Correct Approach

:host([size="l"]) {
  --swc-badge-height: token("component-height-200");
}

πŸ”Ž Badge reference:
Badge size, variant, subtle, and outline states are all expressed via :host() selectors and custom property updates.

πŸ“– See: Component CSS Style Guide β†’ Variant implementation patterns

4. Increasing Selector Specificity to Force Overrides

❌ Anti-Pattern

/* Multiple compounded classes = (0,3,0) */
.swc-Badge.swc-Badge--large.swc-Badge--primary {
  padding: 16px;
}

/* Or stacking to "win" a conflict */
.swc-StatusLight.swc-StatusLight--yellow.swc-StatusLight--sizeL {
  font-size: 20px;
}

Why This Happens

  • Conflicting migrated rules
  • Attempting to preserve visual parity through selector escalation
  • Copying patterns from other codebases that use high specificity

Why This Is a Problem

  • Breaks the (0,1,0) specificity target
  • Makes overrides brittle (e.g. disabled state needs even higher specificity)
  • Hides ordering or layering issues that should be fixed instead

βœ… Correct Approach

  • Fix rule order first
  • Use :where() for compounding selectors
  • Introduce cascade layers only when necessary
/* Before: (0,2,0) */
.swc-Divider--staticWhite.swc-Divider--sizeL {
  --swc-divider-background-color: token("transparent-white-800");
}

/* After: (0,1,0) - rule order determines winner */
.swc-Divider--staticWhite:where(.swc-Divider--sizeL) {
  --swc-divider-background-color: token("transparent-white-800");
}

πŸ”Ž Badge reference:
badge.css uses .swc-Badge--subtle:where(.swc-Badge--gray) for compounded variants. Divider uses the same pattern for static color + size.

πŸ“– See: Component CSS Style Guide β†’ Managing Specificity

5. Using :where() Inside :host() for Custom Property Updates

❌ Anti-Pattern

:host:where([size="l"][variant="primary"]) {
  --swc-badge-height: 40px;
}

Why This Happens

  • Over-application of :where() as a universal fix
  • Assuming specificity controls custom property precedence

Why This Is a Problem

  • Custom properties resolve via inheritance, not specificity
  • This obscures intent and adds complexity

βœ… Correct Approach

:host([size="l"][variant="primary"]) {
  --swc-badge-height: token("component-height-200");
}

πŸ”Ž Badge reference:
Badge safely compounds attributes within :host() when updating custom properties only.

πŸ“– See: Component CSS Style Guide β†’ Shadow DOM specificity and custom property inheritance

6. Exposing Too Many Custom Properties β€œJust in Case”

❌ Anti-Pattern

--swc-badge-border-radius
--swc-badge-gap
--swc-badge-icon-offset

Why This Happens

  • Desire to future-proof
  • Legacy expectations of deep customization

Why This Is a Problem

  • Bloats the public API
  • Makes refactors harder
  • Encourages unsupported overrides

βœ… Correct Approach

  • Expose only what the component itself needs based on its own variant, state, or size requirements
  • Keep mechanical and derived values private
  • Exception: expose properties required for nested component relationships or shared utility styling

πŸ”Ž Badge reference:
Badge exposes a minimal, intentional surface and uses _swc-* properties for derived calculations.

πŸ“– See: Custom Properties Style Guide β†’ Private properties

7. Treating Forced-Colors as a Variant

❌ Anti-Pattern

.swc-Badge {
  border-color: 
    var(--high-contrast-badge-border-color, var(--swc-badge-border-color, token("badge-border-color")));
}

Why This Happens

  • Treating accessibility as a customization hook

Why This Is a Problem

  • Forced-colors must override consumer styles
  • Accessibility takes precedence over customization

βœ… Correct Approach

  • re-use existing component custom property to apply overrides
  • properly order forced-colors at the end of the stylesheet
  • attach to internal class-based selectors
@media (forced-colors: active) {
  .swc-Badge {
    --swc-badge-border-color: CanvasText;
  }
}

πŸ”Ž Status Light reference:
status-light.css overrides --swc-status-light-content-color and adds a border to the dot pseudo-element so it stays visible in high-contrast mode.

πŸ“– See: Component CSS Style Guide β†’ Forced colors requirements

8. Leaving Spectrum-Era Classes After Migration

❌ Anti-Pattern

<div class="swc-Badge spectrum-Badge spectrum-Badge--sizeL">

Why This Happens

  • Incremental migration
  • Hesitation to remove legacy code

Why This Is a Problem

  • Leaves dead code in render()
  • Obscures whether migration is complete
  • Encourages regression

βœ… Correct Approach

  • Remove Spectrum-era classes once CSS migration is complete
  • Treat this as a validation step, not cleanup

πŸ”Ž Badge reference:
After migration, Badge relies solely on .swc-Badge and attributes.

πŸ“– See: Spectrum CSS to SWC Migration β†’ Validation step: removing legacy classes

Before/after refactoring examples

Visual styles on :host β†’ base class

Before After
:host { padding: 8px; background: blue; } :host { display: inline-block; } + .swc-Badge { padding: ...; background: ...; }

Specificity escalation β†’ :where()

Before After
.swc-Badge--subtle.swc-Badge--gray { } .swc-Badge--subtle:where(.swc-Badge--gray) { }

Size classes in render β†’ :host([size])

Before After
class="swc-Badge spectrum-Badge--sizeL" class="swc-Badge" + :host([size="l"]) { --swc-badge-height: ...; }

--mod-* chain β†’ single property

Before After
var(--mod-badge-height, var(--spectrum-badge-height)) var(--swc-badge-height, token("component-height-100"))

9. Nesting compound pseudo-classes on :host() via CSS nesting

❌ Anti-Pattern

/* Intends to target the host in RTL when placement="start" is open */
:host([placement="start"]:popover-open) {
  transform: translateX(calc(-1 * var(--_swc-component-animation-distance)));

  &:dir(rtl) {
    transform: translateX(var(--_swc-component-animation-distance));
  }
}

Why This Happens

CSS nesting with & replaces & with the parent selector. Inside a :host([...]) rule, &:dir(rtl) expands to :host([...]):dir(rtl) β€” a pseudo-class chained after the :host() function. This looks syntactically correct, but browsers do not support compound selectors appended outside of the :host() argument.

Why This Is a Problem

  • The rule silently fails: the :dir() override never applies
  • No lint or parse error is produced, making it hard to detect
  • Properties meant for RTL layout apply in all directions

βœ… Correct Approach

Move all conditions inside the :host() argument as a compound selector:

:host([placement="start"]:popover-open) {
  transform: translateX(calc(-1 * var(--_swc-component-animation-distance)));
}

:host(:dir(rtl)[placement="start"]:popover-open) {
  transform: translateX(var(--_swc-component-animation-distance));
}

Exception: descendants are fine

This restriction only applies when :host() is the outermost element being targeted. When nesting targets a descendant of the host, expanding &:dir(rtl) applies :dir() to the inner element β€” which is valid:

/* βœ… Fine: :dir(rtl) targets .swc-Component-tip, not :host() */
:host([placement="end"]) .swc-Component-tip {
  transform: rotate(45deg);

  &:dir(rtl) {
    transform: rotate(-135deg);
  }
}

Migration note: :dir() in RTL-aware components

:dir() is the most common pseudo-class where this issue surfaces during migrations because RTL overrides are nearly always added after a component's base styles are written. When adding :dir() to any :host-level rule during migration, always write it as a separate :host(:dir(rtl)[...]) rule rather than a nested &:dir(rtl).

10. Size-Specific Custom Properties

❌ Anti-Pattern

:host([size="compact"]) {
  --_swc-accordion-compact-padding-top: token("spacing-100");
}

Why This Happens

  • Attempting to keep size-specific values "private" while still referencing them in variant rules
  • Mapping one custom property per size variant for clarity

Why This Is a Problem

  • A custom property defined on :host([size="compact"]) is part of the component's external style surface β€” the --_swc-* prefix does not make it inaccessible from outside the shadow root
  • Every size requires its own named property, bloating the API surface
  • Consumers cannot override a single "padding-top" concept; they must know and target every size-specific property name

βœ… Correct Approach

Expose a single property on the base and override it per size selector:

.swc-Accordion {
  padding-block-start: var(--swc-accordion-padding-top, token("spacing-200"));
}

:host([size="compact"]) {
  --swc-accordion-padding-top: token("spacing-100");
}

:host([size="spacious"]) {
  --swc-accordion-padding-top: token("spacing-300");
}

Consumers targeting a specific size can still override via attribute selectors on the host:

swc-accordion[size="compact"] {
  --swc-accordion-padding-top: var(--my-compact-spacing);
}

πŸ“– See: Custom Properties Style Guide β†’ Component custom property exposure

11. Suppressing focus outlines with :focus { outline: none }

❌ Anti-Pattern

.swc-Component-header:focus {
  outline: none;
}

Why This Happens

  • Carried over from 1st-gen Spectrum CSS, which managed focus rings through its own system

Why This Is a Problem

  • Removes the focus indicator for keyboard users β€” an accessibility violation (WCAG 2.4.7)
  • The component already has a :focus-visible rule; the :focus suppression just interferes with it

βœ… Correct Approach

  • Remove the rule entirely. :focus-visible already handles when to show the ring; no explicit suppression is needed.

Final Reminder

If you find yourself:

  • adding more classes,
  • increasing selector specificity,
  • or preserving Spectrum-era indirectionβ€”

pause and re-evaluate using the SWC styling model.

The Badge migration demonstrates the intended end state:
explicit customization, reduced indirection, and CSS that works with layout models instead of against them.