Skip to content

Latest commit

 

History

History
260 lines (188 loc) · 10.9 KB

File metadata and controls

260 lines (188 loc) · 10.9 KB

CONTRIBUTOR-DOCS / Style guide / 2nd-Gen CSS / Custom Properties

Custom Properties

In this doc

This guide explains how to manage private, internal, and exposed custom properties in SWC components, and how to use token() to reference design tokens safely.

Naming Conventions

Prefix Purpose
--_swc-* Private, internal custom property
--swc-* Exposed property available for overrides
token("name") Reference to a design token (no prefix)

Private properties are “pseudo-private”: defined on nested shadow elements rather than :host to prevent accidental overrides.

Private Properties

  • Used for repeated, multi-value, or contextually updated properties (themes, states, passthroughs)
  • Always prepended with _ to signal internal use
  • Not directly overrideable by consumers
.swc-Button {
    --_swc-button-background-color: /* value */;
}

CSS custom properties normally can't actually be "private". However, due to shadow DOM encapsulation, we can (partially*) enforce them as private by defining them on a nested wrapper within the component instead of on :host.

Example from Badge — private properties for internal calculations, with exposed properties consumed inline via var():

.swc-Badge {
  --_swc-badge-border-width: token("border-width-200");
  --_swc-badge-padding-inline-start: var(--swc-badge-padding-inline-start, var(--swc-badge-padding-inline, token("component-edge-to-text-100")));
  --_swc-badge-padding-block: var(--swc-badge-padding-block, token("component-padding-vertical-100"));

  padding-inline-start: calc(var(--_swc-badge-padding-inline-start) - var(--_swc-badge-border-width));
  padding-block: calc(var(--_swc-badge-padding-block) - var(--_swc-badge-border-width));
  background: var(--swc-badge-background-color, token("neutral-subdued-background-color-default"));
}

Example from Status Light — private properties as passthroughs for size variants:

.swc-StatusLight {
  --_swc-status-light-padding-block: var(--swc-status-light-padding-block, token("component-padding-vertical-100"));

  padding-block: var(--_swc-status-light-padding-block);
}

*"partially" due to possible eventual exposure when we introduce parts

Reusing private properties

When a private property captures a token value, reference the private property in all subsequent expressions. Do not call token() again for the same value.

.swc-Button {
  --_swc-button-border-width: token("border-width-200");

  /* ✅ References the private var — all derived values update together */
  padding-inline: calc(var(--swc-button-edge-to-text, ...) - var(--_swc-button-border-width));
  border-width: var(--_swc-button-border-width);
}
.swc-Button {
  --_swc-button-border-width: token("border-width-200");

  /* ❌ Repeating token() breaks the single-source relationship */
  padding-inline: calc(var(--swc-button-edge-to-text, ...) - token("border-width-200"));
}

This keeps all overrides and derived calculations linked to the private property. If the definition ever changes, every downstream call updates automatically.

Component Custom Property Exposure

Selector choice encodes API intent: exposed properties are modified via :host(), while internal-only behavior is implemented with internal class selectors.

  • Only expose component properties when needed by the component itself or for passthrough (nested) styling
  • Exposed singularly based on CSS property type, and no longer based on states or variants
  • May be exposed via inclusion in private property, or inline with CSS property
    • Include in private property if value has repeated usage throughout base (non-variant) component styles
  • In migrated components, legacy --mod-* properties should not be preserved; instead, collapse the chain into a single component-level property.

Internal vs. Exposed vs. Static

Type Definition Example
Internal Only via _ or token() --_swc-button-padding-block
Exposed Allows consumer overrides --swc-button-font-size
Static Non-tokenized, fixed CSS display: inline-flex

Static properties are not part of the customization surface and are not expected to change across variants, states, or contexts.

.swc-Button {
    /* Changes per size variant = exposed */
    font-size: var(--swc-button-font-size, token("font-size-200"));
    
    /* Changes per size variant = exposed,
       Multi-value definition = private */
    --_button-padding-block:  
        token("component-top-to-text-100") token("component-bottom-to-text-100");
    padding-block: var(--swc-button-padding-block, 
                        var(--_button-padding-block));
    
    /* Does not change = internal
       Future parts will offer ability to modify */
    min-inline-size: token("button-minimum-width-multiplier");
    
    /* Non-tokenized CSS properties = static */
    display: inline-flex;
}

/* Library style for component "large" variant via :host() selector to maintain consumer override capability */
:host([size="l"]) {
    --swc-button-font-size: token("font-size-400");
    --swc-button-padding-block: 
        token("component-top-to-text-200") token("component-bottom-to-text-200");
}

/* Consumer override */
sp-button[size="l"] {
    --swc-button-font-size: var(--swc-font-size-500);
}

Decision Tree for Exposure

flowchart TD
    A[Property changes per variant/attribute/state?] -->|Yes| B[Expose property]
    A -->|No| C[Changes for CJK?]
    C -->|Yes| B
    C -->|No| D[Changes for WHCM?]
    D -->|Yes| B
    D -->|No| E[Keep internal]
Loading

For nested component relationships, expose only if dependent on the base library and not legacy consumer customization.

For examples of exposed vs internal properties applied via selectors, see:

Exclusions for Custom Property Overrides

There are some exclusions as to what should be exposed for overrides:

  • color properties for static white and static black variants
    • ensures intent of color contrast
  • color properties when tied to non-semantic color palette variants
    • example: the magenta variant of Badge
    • consumers will be encouraged to instead add a global override to re-assign those color values instead
  • certain geometric variants (e.g. fixed-edge Badge) intentionally override exposed properties, such as corner radius
  • properties modified for forced-colors mode
    • ensures forced-colors related overrides take precedence over consumer overrides for base component
    • forced-colors overrides are applied at the end of the component stylesheet. See the forced-colors section in the Component CSS Style Guide.

Use internal selectors (ex. .swc-Badge--magenta ) to pass library overrides for these exclusions.

Selector Conventions

Exposed properties require :host() to maintain override capability:

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

Consumers can then override exposed properties based on attributes and states:

swc-button[size="s"]
swc-button[aria-expanded]
swc-button:focus-visible

Variant Selectors and Inheritance

Refer to the Component CSS Style Guide for more details about variant selectors and how they are impacted by custom property inheritance.

Using token()

  • Provides dynamic resolution from design tokens
  • Can be used as full or partial values in CSS
  • Require a token name without the prefix

Use of token() in CSS values such as the following:

.swc-Button {
    background-color: token("accent-background-color-default");
}

Will be transformed at build time into valid CSS values:

.swc-Button {
    background-color: var(--swc-accent-background-color-default);
}

More examples and further information on how token() retrieves and processes token data can be found in the README for @adobe/postcss-token ( swc/tools/postcss-token ).

Troubleshooting token()

Error Cause Action
token() not found Typo, prefix, deprecated Remove prefix, check spelling, consult debug-tokens.txt
Invalid token value Cannot resolve to CSS Verify against S2 Token Specs; possibly add as custom global token
  • Debug log: yarn debug:tokens (from @adobe/swc-tokens)
  • Deprecated tokens are logged with [DEPRECATED]

Adding Global Tokens

Additional global tokens or token overrides may be necessary if values are unique to SWC, and not available - currently or planned - in the design token source package, @adobe/spectrum-tokens.

Examples of current custom global tokens include global animation transition timings and web-friendly font stacks.

Adding global tokens means that they can be accessed via token() and included correctly within the unified stylesheet for downstream consumer use as well.

For instructions on adding global tokens, refer to "Custom Tokens and Overrides" in the README for @adobe/swc-tokens (swc/tools/swc-tokens).