The essentials
Quick reference
One focused task per row. Jump to the related section for complete, working examples.
| Use | Syntax | Examples |
|---|---|---|
| Create a scope | @scope (.card) { h2 { color: var(--heading-color); } } | View examples |
| Stop before nested regions | @scope (.article) to (.comments) {
a { text-decoration-thickness: .12em; };
} | View examples |
| Select the scoping root | @scope (.notice) {
:scope { border-inline-start: .25rem solid; };
} | View examples |
| Relate content to the root | @scope (.menu) { :scope > a { display: block; } } | View examples |
| Prefer the nearest scope | @scope (.theme) { p { color: var(--text); } } | View examples |
| Keep a selector fallback | .profile .name { color: #17324d; } | View examples |
| Style the shadow host | :host {
display: block;
color: var(--profile-color, #172033);
} | View examples |
| Match a host state | :host([variant='compact']) { font-size: .875rem; } | View examples |
| Style assigned elements | ::slotted(a) { color: var(--profile-link, #075985); } | View examples |
| Expose a shadow part | <button part="control">Save</button> | View examples |
| Style an exposed part | save-button::part(control) { border-radius: .5rem; } | View examples |
| Assign multiple part names | <span part="label emphasized">Important</span> | View examples |
| Style a part interaction | save-button::part(control):focus-visible {
outline: 3px solid #f59e0b;
} | View examples |
| Forward a nested part | <icon-button exportparts="control"></icon-button> | View examples |
| Rename a forwarded part | <icon-button exportparts="control:
action"></icon-button> | View examples |
| Offer a theme token | status-chip { --status-accent: #0f766e; } | View examples |
| Preserve system colors | @media (forced-colors: active) {
status-chip::part(icon) { color: ButtonText; };
} | View examples |
| Contain independent layout | :host { contain: layout paint; } | View examples |
CSS scoping solves two related but distinct problems. The @scope rule limits ordinary document selectors to a subtree, while Shadow DOM creates an encapsulation boundary with explicit styling hooks such as :host, ::slotted(), custom properties, and ::part(). Treat exposed part names and custom properties as a versioned component API: expose stable roles rather than implementation details, preserve native semantics and focus indicators, and layer enhancements over a usable fallback. Some scoping and forwarding details continue to evolve, so verify behavior in the browsers embedded by your product.
Step by step
Detailed examples
Set explicit upper and lower boundaries
The selector after @scope creates one scoping root for every match. Rules inside can match that inclusive subtree, while an optional to() selector creates lower boundaries that are excluded together with their descendants. Use :scope when the rule must target or express a relationship to the current root. A scope changes selector reach; it does not create Shadow DOM, isolate inheritance, or rename classes.
@scope (.article) to (.comments) {
:scope {
color: #172033;
line-height: 1.65;
}
:scope > h2 {
color: #0f4c5c;
}
a {
text-decoration-thickness: .12em;
}
} Account for scope proximity and unsupported at-rules
Scoped declarations still participate in the cascade. When origin, importance, context, style attribute, layer, and specificity are tied, the declaration whose scoping root is closest to the subject wins before order of appearance is considered. The selector that establishes the root does not add specificity to selectors inside. Keep essential conventional rules outside @scope; browsers that do not recognize the at-rule ignore that block, while supporting browsers can apply the localized enhancement.
.profile .name {
color: #17324d;
}
@scope (.profile) {
.name { color: var(--profile-name, #075985); }
@scope (.featured) {
.name { color: var(--featured-name, #9f1239); }
}
} Style the host and projected content from inside
Inside a shadow stylesheet, :host selects the custom element and :host(...) restricts it by a public host condition. ::slotted() styles only elements directly assigned to a slot; it cannot reach descendants inside an assigned element, and text nodes are not selectable. Prefer meaningful attributes and custom states for variants. Keep required semantics in actual HTML because CSS encapsulation does not supply labels, roles, keyboard behavior, or focus management.
:host {
display: block;
color: var(--profile-color, #172033);
}
:host([variant='compact']) {
font-size: .875rem;
}
::slotted(a) {
color: var(--profile-link, #075985);
}
/* Style descendants of that link in the light-DOM stylesheet. */ Expose stable styling roles with part and ::part()
A part attribute assigns one or more public names to an element in a shadow tree. An outer stylesheet selects those elements through the shadow host with ::part(). Part names resemble classes rather than IDs, so several elements may share a role. Consumers cannot append descendant selectors to inspect internal structure; they can apply supported pseudo-classes such as :hover or :focus-visible to the selected part. Name roles such as control, label, and icon so refactoring the internal tags does not break the contract.
<save-button>
<template shadowrootmode="open">
<button part="control primary-control" type="button">
<span part="label">Save changes</span>
</button>
</template>
</save-button> save-button::part(control) {
border: 0;
border-radius: .5rem;
padding: .7rem 1rem;
}
save-button::part(control):focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
save-button::part(label) {
font-weight: 700;
} Forward only intentional parts through nested hosts
A part exposed by an inner component does not automatically cross another shadow boundary. Put exportparts on the nested shadow host to forward selected names, optionally mapping an inner name to a different outer name with inner: outer syntax. The attribute is a comma-separated list of mappings. Forward the smallest stable surface needed by consumers; exporting every internal role couples multiple component versions and makes safe refactoring harder.
<tool-bar>
<template shadowrootmode="open">
<icon-button exportparts="control: action, icon">
<template shadowrootmode="open">
<button part="control" type="button">
<span part="icon" aria-hidden="true">+</span>
<span>New item</span>
</button>
</template>
</icon-button>
</template>
</tool-bar> tool-bar::part(action) {
min-block-size: 2.75rem;
padding-inline: 1rem;
}
tool-bar::part(icon) {
font-size: 1.25rem;
} Design theme hooks that preserve accessible states
Custom properties cross the shadow boundary through normal inheritance and are usually better than parts for values such as colors, spacing, and type scales. Parts are appropriate when consumers must style a whole exposed surface. Supply readable defaults, retain visible focus, and test forced colors, zoom, long translations, and reduced motion. Do not allow a token to make essential text transparent or shrink an interactive target below the product's accessibility requirements.
status-chip {
--status-accent: #0f766e;
--status-gap: .5rem;
}
status-chip::part(control):focus-visible {
outline: 3px solid currentColor;
outline-offset: 2px;
}
@media (forced-colors: active) {
status-chip::part(control) { border: 1px solid ButtonText; }
status-chip::part(icon) { color: ButtonText; }
} Progressively enhance and keep boundaries inexpensive
Unknown at-rules and selectors can cause rules to be discarded, so keep the unscoped layout and the component's semantic behavior usable first. Shadow DOM is an encapsulation tool, not an automatic performance optimization; large component counts, expensive selectors, and repeated style text still have costs. Reuse constructable stylesheets when JavaScript architecture and browser targets permit it. Apply containment only after verifying overlays, focus rings, sticky descendants, and intrinsic sizing are not clipped or changed. Re-test @scope and exportparts behavior as their specifications and implementations mature.
:host {
display: block;
}
@supports (contain: layout paint) {
:host([contained]) {
contain: layout paint;
}
}
/* Do not opt in when popovers or focus decoration must paint outside. */ Local code tester
Theme a scoped card and an exposed shadow part
Edit the scope rules, component token, and ::part() styles while preserving the native button's focus treatment.
Sources and further reading
References
Authoritative documentation used to verify and expand this cheat sheet.
Help us improve
Found a typo or missing example?
Tell us what would make this cheat sheet clearer, more complete, or more useful.



