CONTRIBUTOR-DOCS / Style guide / 2nd-Gen CSS / Styling Anti-Patterns (What to Avoid)
In this doc
- 1. Leaving Visual Styles on
:host - 2. Preserving
--mod-*as an Extra Indirection Layer - 3. Excess Variant Classes in
render() - 4. Increasing Selector Specificity to Force Overrides
- 5. Using
:where()Inside:host()for Custom Property Updates - 6. Exposing Too Many Custom Properties “Just in Case”
- 7. Treating Forced-Colors as a Variant
- 8. Leaving Spectrum-Era Classes After Migration
- Before/after refactoring examples
- 9. Suppressing focus outlines with
:focus { outline: none } - Final Reminder
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
:host {
padding: 8px;
background-color: var(--spectrum-badge-background-color-default);
}- Spectrum CSS often treated the custom element root as the primary styling surface
- Incremental migrations make it tempting to keep existing rules in place
:hostis part of the public styling API- Visual styles here are harder to override predictably
- This breaks the SWC model where
:hostdefines layout participation only
: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
This anti-pattern reflects one of the most common and subtle migration mistakes.
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"));--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
--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
- Remove
--mod-*entirely - Collapse the fallback chain into a single component custom property
- Decide whether the property should be:
- exposed (
--swc-*) - or internal (
--_swc-*)
- exposed (
- 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
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',
})- Legacy Spectrum class-based patterns
- Uncertainty about expressing variants via attributes
- Duplicates logic already expressed by attributes
- Extranneous when class not actually being used as a style hook
: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
/* 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;
}- Conflicting migrated rules
- Attempting to preserve visual parity through selector escalation
- Copying patterns from other codebases that use high specificity
- 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
- 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
:host:where([size="l"][variant="primary"]) {
--swc-badge-height: 40px;
}- Over-application of
:where()as a universal fix - Assuming specificity controls custom property precedence
- Custom properties resolve via inheritance, not specificity
- This obscures intent and adds complexity
: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
--swc-badge-border-radius
--swc-badge-gap
--swc-badge-icon-offset- Desire to future-proof
- Legacy expectations of deep customization
- Bloats the public API
- Makes refactors harder
- Encourages unsupported overrides
- Expose only what the component itself needs
- Keep mechanical and derived values private
🔎 Badge reference:
Badge exposes a minimal, intentional surface and uses _swc-* properties for derived calculations.
📖 See: Custom Properties Style Guide → Private properties
.swc-Badge {
border-color:
var(--high-contrast-badge-border-color, var(--swc-badge-border-color, token("badge-border-color")));
}- Treating accessibility as a customization hook
- Forced-colors must override consumer styles
- Accessibility takes precedence over customization
- 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
<div class="swc-Badge spectrum-Badge spectrum-Badge--sizeL">- Incremental migration
- Hesitation to remove legacy code
- Leaves dead code in
render() - Obscures whether migration is complete
- Encourages regression
- 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 |
|---|---|
:host { padding: 8px; background: blue; } |
:host { display: inline-block; } + .swc-Badge { padding: ...; background: ...; } |
| Before | After |
|---|---|
.swc-Badge--subtle.swc-Badge--gray { } |
.swc-Badge--subtle:where(.swc-Badge--gray) { } |
| Before | After |
|---|---|
class="swc-Badge spectrum-Badge--sizeL" |
class="swc-Badge" + :host([size="l"]) { --swc-badge-height: ...; } |
| Before | After |
|---|---|
var(--mod-badge-height, var(--spectrum-badge-height)) |
var(--swc-badge-height, token("component-height-100")) |
.swc-Component-header:focus {
outline: none;
}- Carried over from 1st-gen Spectrum CSS, which managed focus rings through its own system
- Removes the focus indicator for keyboard users — an accessibility violation (WCAG 2.4.7)
- The component already has a
:focus-visiblerule; the:focussuppression just interferes with it
- Remove the rule entirely.
:focus-visiblealready handles when to show the ring; no explicit suppression is needed.
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.