CONTRIBUTOR-DOCS / Style guide / 2nd-Gen CSS / 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.
| 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
:hostto prevent accidental overrides.
- 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
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.
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
- This distinction directly affects which selector type is used (
:host()vs internal class selectors). See Variant Selectors and Inheritance.
- This distinction directly affects which selector type is used (
- 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.
| 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);
}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]
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:
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
magentavariant of Badge - consumers will be encouraged to instead add a global override to re-assign those color values instead
- example: the
- 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.
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-visibleRefer to the Component CSS Style Guide for more details about variant selectors and how they are impacted by custom property inheritance.
- 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 ).
| 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]
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).