CSS @scope Theming
Two theme regions sharing identical class names, styled independently with native CSS @scope so tokens and rules never leak across subtrees — plus a donut-scope example that stops at a boundary.
MCP
Code
:root {
--bg: #0c0d10;
--surface: #15161b;
--surface-2: #1d1f27;
--ink: #e9eaf0;
--muted: #9a9cab;
--accent: #8b5cf6;
--accent-2: #22d3ee;
--ok: #34d399;
--warn: #fbbf24;
--danger: #f87171;
--line: rgba(255, 255, 255, 0.10);
--line-2: rgba(255, 255, 255, 0.18);
--r-sm: 8px;
--r-md: 12px;
--r-lg: 16px;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
background:
radial-gradient(900px 500px at 15% -10%, rgba(139, 92, 246, 0.14), transparent 60%),
radial-gradient(800px 500px at 110% 0%, rgba(34, 211, 238, 0.10), transparent 55%),
var(--bg);
color: var(--ink);
font-family: "Inter", system-ui, -apple-system, sans-serif;
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
min-height: 100vh;
}
code,
pre {
font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
}
.wrap {
max-width: 940px;
margin: 0 auto;
padding: 40px 22px 72px;
}
/* ---------- Masthead ---------- */
.masthead {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 18px;
flex-wrap: wrap;
margin-bottom: 30px;
}
.kicker {
display: inline-block;
font-size: 12px;
font-weight: 600;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--accent-2);
margin-bottom: 8px;
}
.masthead h1 {
margin: 0 0 12px;
font-size: clamp(26px, 5vw, 38px);
font-weight: 700;
letter-spacing: -0.02em;
}
.masthead h1 code {
color: var(--accent);
background: rgba(139, 92, 246, 0.12);
padding: 0 8px;
border-radius: var(--r-sm);
}
.lede {
margin: 0;
max-width: 560px;
color: var(--muted);
font-size: 15px;
}
.lede code {
color: var(--ink);
background: var(--surface-2);
padding: 1px 6px;
border-radius: 6px;
font-size: 0.86em;
}
.support {
display: inline-flex;
align-items: center;
gap: 9px;
padding: 9px 14px;
border: 1px solid var(--line);
border-radius: 999px;
background: var(--surface);
font-size: 13px;
font-weight: 500;
white-space: nowrap;
}
.support__dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--muted);
box-shadow: 0 0 0 4px rgba(255, 255, 255, 0.04);
}
.support.is-native .support__dot {
background: var(--ok);
box-shadow: 0 0 0 4px rgba(52, 211, 153, 0.16);
}
.support.is-fallback .support__dot {
background: var(--warn);
box-shadow: 0 0 0 4px rgba(251, 191, 36, 0.16);
}
.support code {
color: var(--ink);
}
/* ---------- Region grid ---------- */
.regions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 18px;
margin-bottom: 34px;
}
.region {
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
padding: 16px;
min-width: 0;
}
.region__bar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.region__name {
display: inline-flex;
align-items: center;
gap: 9px;
margin: 0;
font-size: 14px;
font-weight: 600;
color: var(--muted);
}
.swatch {
width: 14px;
height: 14px;
border-radius: 5px;
border: 1px solid var(--line-2);
}
.hue {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--muted);
font-weight: 500;
}
.hue__input {
-webkit-appearance: none;
appearance: none;
width: 108px;
height: 6px;
border-radius: 999px;
background: linear-gradient(90deg, #f87171, #fbbf24, #34d399, #22d3ee, #8b5cf6, #f87171);
outline: none;
cursor: pointer;
}
.hue__input::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--ink);
border: 2px solid var(--bg);
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
cursor: pointer;
}
.hue__input::-moz-range-thumb {
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--ink);
border: 2px solid var(--bg);
cursor: pointer;
}
.hue__input:focus-visible {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.5);
}
/* ---------- The shared .card component (neutral defaults) ----------
These are the fallback styles. When @scope is supported the scoped
blocks injected by JS override these per-region without any leakage. */
.card {
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface-2);
padding: 18px;
transition: transform 0.18s ease, box-shadow 0.18s ease, border-color 0.18s ease;
}
.card:hover {
transform: translateY(-3px);
}
.card__top {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
}
.badge {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.08);
color: var(--ink);
}
.ghost {
border: 1px solid var(--line);
background: transparent;
color: var(--muted);
width: 32px;
height: 32px;
border-radius: 9px;
font-size: 15px;
cursor: pointer;
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
}
.ghost:hover {
color: var(--ink);
border-color: var(--line-2);
}
.ghost:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
.card__title {
margin: 0 0 4px;
font-size: 20px;
font-weight: 700;
letter-spacing: -0.01em;
}
.card__meta {
margin: 0 0 14px;
font-size: 13px;
color: var(--muted);
}
.card__list {
list-style: none;
margin: 0 0 16px;
padding: 0;
display: grid;
gap: 9px;
font-size: 13.5px;
}
.card__list li {
display: flex;
align-items: center;
gap: 9px;
color: var(--ink);
}
.card__list code {
font-size: 0.85em;
background: rgba(0, 0, 0, 0.25);
padding: 0 5px;
border-radius: 5px;
}
.tick {
display: inline-grid;
place-items: center;
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
color: var(--ink);
font-size: 11px;
font-weight: 700;
flex: none;
}
.card__foot {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.price {
font-size: 22px;
font-weight: 700;
}
.price small {
font-size: 12px;
font-weight: 500;
color: var(--muted);
}
.cta {
border: none;
cursor: pointer;
padding: 10px 16px;
border-radius: var(--r-sm);
font: inherit;
font-weight: 600;
font-size: 13.5px;
color: #0b0b10;
background: var(--ink);
transition: transform 0.14s ease, filter 0.14s ease, box-shadow 0.14s ease;
}
.cta:hover {
transform: translateY(-1px);
}
.cta:active {
transform: translateY(0);
}
.cta:focus-visible {
outline: 2px solid var(--accent-2);
outline-offset: 2px;
}
/* ---------- Fallback attribute theming (no @scope support) ---------- */
[data-brand="a"] .badge,
[data-brand="a"] .cta,
[data-brand="a"] .tick,
[data-brand="b"] .badge,
[data-brand="b"] .cta,
[data-brand="b"] .tick {
background: var(--brand-accent, var(--ink));
}
[data-brand] .badge,
[data-brand] .tick {
color: var(--brand-ink, #0b0b10);
}
[data-brand] .cta {
color: var(--brand-ink, #0b0b10);
box-shadow: 0 8px 22px -12px var(--brand-glow, transparent);
}
[data-brand] .card {
border-color: var(--brand-line, var(--line));
box-shadow: 0 20px 50px -34px var(--brand-glow, transparent);
}
[data-brand] .card:hover {
border-color: var(--brand-accent, var(--line-2));
}
[data-brand] .card__title {
color: var(--brand-title, var(--ink));
}
[data-brand] .price {
color: var(--brand-title, var(--ink));
}
[data-brand] .swatch,
[data-brand] .region__name .swatch {
background: var(--brand-accent, var(--ink));
}
/* ---------- Donut section ---------- */
.donut-section {
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
padding: 20px;
margin-bottom: 34px;
}
.donut-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 16px;
}
.donut-head h2 {
margin: 0 0 6px;
font-size: 18px;
font-weight: 700;
}
.donut-head h2 code,
.donut-copy code,
.source__bar code,
.source__hint code {
color: var(--accent-2);
font-size: 0.86em;
}
.donut-copy {
margin: 0;
max-width: 520px;
color: var(--muted);
font-size: 13.5px;
}
.toggle {
display: inline-flex;
align-items: center;
gap: 10px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: var(--muted);
user-select: none;
}
.toggle input {
position: absolute;
opacity: 0;
width: 0;
height: 0;
}
.toggle__track {
width: 42px;
height: 24px;
border-radius: 999px;
background: var(--surface-2);
border: 1px solid var(--line-2);
position: relative;
transition: background 0.18s ease, border-color 0.18s ease;
}
.toggle__thumb {
position: absolute;
top: 2px;
left: 2px;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--muted);
transition: transform 0.18s ease, background 0.18s ease;
}
.toggle input:checked + .toggle__track {
background: rgba(34, 211, 238, 0.22);
border-color: var(--accent-2);
}
.toggle input:checked + .toggle__track .toggle__thumb {
transform: translateX(18px);
background: var(--accent-2);
}
.toggle input:focus-visible + .toggle__track {
box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.4);
}
/* thread */
.thread {
border-radius: var(--r-md);
}
.note {
border: 1px solid var(--line);
border-radius: var(--r-md);
background: var(--surface-2);
padding: 16px 16px 6px;
}
.note__author {
margin: 0 0 6px;
font-size: 12px;
font-weight: 600;
color: var(--muted);
font-family: "JetBrains Mono", monospace;
}
.note__body {
margin: 0 0 12px;
font-size: 14px;
}
.note__link {
display: inline-block;
margin-bottom: 14px;
font-size: 13px;
font-weight: 600;
color: var(--ink);
text-decoration: none;
border-bottom: 1px dashed var(--line-2);
padding-bottom: 1px;
}
.quote {
margin: 4px 0 14px;
padding: 14px 14px 4px;
border-left: 3px solid var(--line-2);
border-radius: 0 var(--r-sm) var(--r-sm) 0;
background: rgba(0, 0, 0, 0.22);
}
/* ---------- Live source ---------- */
.source {
border: 1px solid var(--line);
border-radius: var(--r-lg);
background: var(--surface);
overflow: hidden;
}
.source__bar {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 10px;
flex-wrap: wrap;
padding: 14px 18px;
border-bottom: 1px solid var(--line);
}
.source__bar h2 {
margin: 0;
font-size: 14px;
font-weight: 600;
}
.source__hint {
font-size: 12px;
color: var(--muted);
}
.source__code {
margin: 0;
padding: 16px 18px;
overflow-x: auto;
font-size: 12.5px;
line-height: 1.65;
color: #d6d8e4;
background: #0a0b0e;
}
.source__code .k { color: var(--accent); }
.source__code .s { color: var(--accent-2); }
.source__code .p { color: var(--ok); }
.source__code .c { color: var(--muted); }
/* ---------- Responsive ---------- */
@media (max-width: 520px) {
.wrap { padding: 28px 15px 56px; }
.regions { grid-template-columns: 1fr; }
.support { align-self: flex-start; }
.hue__input { width: 92px; }
}(function () {
"use strict";
// --- Feature detection: does the engine support the CSS @scope at-rule? ---
// CSS.supports("at-rule") isn't broadly available, so probe by injecting a
// scoped rule into a real stylesheet and reading back whether a scoped
// element actually receives the declaration.
function detectScope() {
if (typeof CSS !== "undefined" && typeof CSS.supports === "function") {
try {
// Some engines expose at-rule support directly.
if (CSS.supports("at-rule(@scope)")) return true;
} catch (_) {
/* fall through to runtime probe */
}
}
try {
var probe = document.createElement("div");
probe.setAttribute("data-scope-probe", "");
probe.style.position = "absolute";
probe.style.opacity = "0";
probe.style.pointerEvents = "none";
var inner = document.createElement("span");
probe.appendChild(inner);
document.body.appendChild(probe);
var style = document.createElement("style");
style.textContent =
"@scope ([data-scope-probe]) { :scope span { --scope-ok: 1; outline-width: 3px; } }";
document.head.appendChild(style);
var val = getComputedStyle(inner).outlineWidth;
var ok = val === "3px";
document.head.removeChild(style);
document.body.removeChild(probe);
return ok;
} catch (_) {
return false;
}
}
var NATIVE = detectScope();
// --- Support badge ---
var support = document.getElementById("support");
var supportLabel = support.querySelector(".support__label");
if (NATIVE) {
support.classList.add("is-native");
supportLabel.innerHTML = "Native <code>@scope</code> active";
} else {
support.classList.add("is-fallback");
supportLabel.innerHTML = "Fallback: attribute scoping";
}
// --- Live theme state, per brand ---
var brands = {
a: { hue: 266, sat: 78 },
b: { hue: 18, sat: 82 },
};
// One <style> element that holds the generated @scope (native) or
// :root-var (fallback) rules for both brands.
var themeStyle = document.createElement("style");
themeStyle.id = "scope-theme";
document.head.appendChild(themeStyle);
function hsl(h, s, l) {
return "hsl(" + h + " " + s + "% " + l + "%)";
}
// Build the tokens object for a brand from its hue/sat.
function tokensFor(b) {
var h = b.hue;
var s = b.sat;
return {
accent: hsl(h, s, 62),
accentSoft: "hsl(" + h + " " + s + "% 62% / 0.16)",
line: "hsl(" + h + " " + s + "% 62% / 0.35)",
title: hsl(h, Math.min(s + 6, 100), 82),
glow: "hsl(" + h + " " + s + "% 55% / 0.55)",
ink: "#0b0b10",
};
}
// Produce the actual CSS text. In native mode we emit real @scope blocks so
// the .card / .badge / .cta classes are re-styled ONLY inside .brand-a and
// .brand-b — proving there's no leakage even though both use the same names.
function buildCSS() {
var out = "";
["a", "b"].forEach(function (key) {
var t = tokensFor(brands[key]);
var sel = ".brand-" + key;
if (NATIVE) {
out +=
"@scope (" + sel + ") {\n" +
" .swatch { background: " + t.accent + "; border-color: " + t.line + "; }\n" +
" .card { border-color: " + t.line + "; box-shadow: 0 22px 55px -34px " + t.glow + "; }\n" +
" .card:hover { border-color: " + t.accent + "; box-shadow: 0 26px 60px -30px " + t.glow + "; }\n" +
" .card__title, .price { color: " + t.title + "; }\n" +
" .badge { background: " + t.accentSoft + "; color: " + t.title + "; }\n" +
" .tick { background: " + t.accent + "; color: " + t.ink + "; }\n" +
" .cta { background: " + t.accent + "; color: " + t.ink + "; box-shadow: 0 10px 26px -12px " + t.glow + "; }\n" +
" .cta:hover { filter: brightness(1.08); }\n" +
" .ghost:hover { border-color: " + t.accent + "; color: " + t.title + "; }\n" +
"}\n";
} else {
out +=
sel + " {\n" +
" --brand-accent: " + t.accent + ";\n" +
" --brand-line: " + t.line + ";\n" +
" --brand-title: " + t.title + ";\n" +
" --brand-glow: " + t.glow + ";\n" +
" --brand-ink: " + t.ink + ";\n" +
"}\n";
}
});
return out;
}
function applyTheme() {
themeStyle.textContent = buildCSS();
// keep swatches in sync in fallback mode too
updateSwatches();
renderSource();
}
function updateSwatches() {
["a", "b"].forEach(function (key) {
var region = document.querySelector('[data-brand="' + key + '"]');
if (!region) return;
var t = tokensFor(brands[key]);
var sw = region.querySelector(".swatch");
if (sw && !NATIVE) sw.style.background = t.accent;
});
}
// --- Syntax-highlighted preview of the generated rule for brand A ---
var cssOut = document.getElementById("cssOut");
function esc(str) {
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
}
function renderSource() {
var t = tokensFor(brands.a);
var raw;
if (NATIVE) {
raw =
"/* Brand A — same class names, fenced by @scope */\n" +
"@scope (.brand-a) {\n" +
" .card { border-color: " + t.line + "; }\n" +
" .badge { background: " + t.accentSoft + "; }\n" +
" .cta { background: " + t.accent + "; }\n" +
"}\n\n" +
"@scope (.brand-b) {\n" +
" .cta { background: " + tokensFor(brands.b).accent + "; }\n" +
"}\n\n" +
"/* Donut: theme the thread but stop at the quote */\n" +
"@scope (.thread) to (.quote) {\n" +
" .note__link { color: var(--accent-2); }\n" +
"}";
} else {
raw =
"/* @scope not supported — attribute fallback */\n" +
".brand-a { --brand-accent: " + t.accent + "; }\n" +
".brand-b { --brand-accent: " + tokensFor(brands.b).accent + "; }\n\n" +
"[data-brand] .cta { background: var(--brand-accent); }";
}
// lightweight tokenizer for display only
var html = esc(raw)
.replace(/(\/\*[\s\S]*?\*\/)/g, '<span class="c">$1</span>')
.replace(/(@scope|to)\b/g, '<span class="k">$1</span>')
.replace(/(hsl\([^)]*\))/g, '<span class="s">$1</span>')
.replace(/(\.[a-z][\w-]*|:scope|\[data-brand\])/g, '<span class="p">$1</span>');
cssOut.innerHTML = html;
}
// --- Wire up hue sliders ---
document.querySelectorAll(".hue__input").forEach(function (input) {
var key = input.getAttribute("data-hue");
input.addEventListener("input", function () {
brands[key].hue = Number(input.value);
applyTheme();
});
});
// --- Donut hole toggle ---
var donutToggle = document.getElementById("donutToggle");
var thread = document.getElementById("thread");
// Style element that supplies the donut-scoped theme.
var donutStyle = document.createElement("style");
donutStyle.id = "donut-theme";
document.head.appendChild(donutStyle);
function applyDonut() {
var on = donutToggle.checked;
thread.setAttribute("data-hole", on ? "on" : "off");
if (NATIVE) {
if (on) {
// Donut scope: applies to .thread down to (but not including) .quote.
donutStyle.textContent =
"@scope (#thread) to (.quote) {\n" +
" .note { border-color: rgba(34,211,238,0.4); box-shadow: 0 0 0 1px rgba(34,211,238,0.12) inset; }\n" +
" .note__link { color: var(--accent-2); border-bottom-color: rgba(34,211,238,0.6); }\n" +
" .note__body { color: #dff6fb; }\n" +
"}\n";
} else {
// No lower boundary => the quote gets themed too (hole closed).
donutStyle.textContent =
"@scope (#thread) {\n" +
" .note, .quote { border-color: rgba(34,211,238,0.4); }\n" +
" .note__link { color: var(--accent-2); border-bottom-color: rgba(34,211,238,0.6); }\n" +
" .note__body { color: #dff6fb; }\n" +
"}\n";
}
} else {
// Fallback: emulate donut with :not() descendant selectors.
if (on) {
donutStyle.textContent =
"#thread .note > .note__link, #thread .note > .note__body { }\n" +
"#thread > .note { border-color: rgba(34,211,238,0.4); }\n" +
"#thread > .note > .note__link { color: var(--accent-2); border-bottom-color: rgba(34,211,238,0.6); }\n" +
"#thread > .note > .note__body { color: #dff6fb; }\n";
} else {
donutStyle.textContent =
"#thread .note__link { color: var(--accent-2); border-bottom-color: rgba(34,211,238,0.6); }\n" +
"#thread .note__body { color: #dff6fb; }\n" +
"#thread .note, #thread .quote { border-color: rgba(34,211,238,0.4); }\n";
}
}
}
donutToggle.addEventListener("change", applyDonut);
// --- Card CTA feedback ---
document.querySelectorAll(".cta").forEach(function (btn) {
btn.addEventListener("click", function () {
var original = btn.textContent;
btn.textContent = "Selected ✓";
btn.disabled = true;
setTimeout(function () {
btn.textContent = original;
btn.disabled = false;
}, 1400);
});
});
// --- Bookmark ghost toggle ---
document.querySelectorAll(".ghost").forEach(function (btn) {
btn.addEventListener("click", function () {
var on = btn.getAttribute("aria-pressed") === "true";
btn.setAttribute("aria-pressed", String(!on));
btn.textContent = on ? "☆" : "★";
});
});
// --- Init ---
applyTheme();
applyDonut();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>CSS @scope Theming</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<main class="wrap">
<header class="masthead">
<div class="masthead__title">
<span class="kicker">CSS Cascade</span>
<h1><code>@scope</code> Theming</h1>
<p class="lede">
Two regions. Identical class names — <code>.card</code>, <code>.badge</code>,
<code>.cta</code>. Zero leakage, because each subtree is fenced off with native
<code>@scope</code> and its own theme tokens.
</p>
</div>
<div class="support" id="support" role="status" aria-live="polite">
<span class="support__dot" aria-hidden="true"></span>
<span class="support__label">Detecting <code>@scope</code>…</span>
</div>
</header>
<!-- ============ Twin brand regions ============ -->
<section class="regions" aria-label="Two independently themed regions">
<!-- BRAND A -->
<article class="region brand-a" data-brand="a" aria-labelledby="brand-a-h">
<div class="region__bar">
<h2 class="region__name" id="brand-a-h">
<span class="swatch" aria-hidden="true"></span> Brand A · Aurora
</h2>
<label class="hue">
<span>Accent</span>
<input type="range" class="hue__input" data-hue="a" min="0" max="360" value="266"
aria-label="Brand A accent hue" />
</label>
</div>
<div class="card">
<div class="card__top">
<div class="badge">Pro</div>
<button class="ghost" type="button" aria-label="Bookmark Brand A plan">☆</button>
</div>
<h3 class="card__title">Nebula Plan</h3>
<p class="card__meta">Same markup, Brand A tokens.</p>
<ul class="card__list">
<li><span class="tick" aria-hidden="true">✓</span> Scoped design tokens</li>
<li><span class="tick" aria-hidden="true">✓</span> No class-name collisions</li>
<li><span class="tick" aria-hidden="true">✓</span> No <code>!important</code></li>
</ul>
<div class="card__foot">
<span class="price">$24<small>/mo</small></span>
<button class="cta" type="button">Choose Aurora</button>
</div>
</div>
</article>
<!-- BRAND B -->
<article class="region brand-b" data-brand="b" aria-labelledby="brand-b-h">
<div class="region__bar">
<h2 class="region__name" id="brand-b-h">
<span class="swatch" aria-hidden="true"></span> Brand B · Ember
</h2>
<label class="hue">
<span>Accent</span>
<input type="range" class="hue__input" data-hue="b" min="0" max="360" value="18"
aria-label="Brand B accent hue" />
</label>
</div>
<div class="card">
<div class="card__top">
<div class="badge">Pro</div>
<button class="ghost" type="button" aria-label="Bookmark Brand B plan">☆</button>
</div>
<h3 class="card__title">Nebula Plan</h3>
<p class="card__meta">Same markup, Brand B tokens.</p>
<ul class="card__list">
<li><span class="tick" aria-hidden="true">✓</span> Scoped design tokens</li>
<li><span class="tick" aria-hidden="true">✓</span> No class-name collisions</li>
<li><span class="tick" aria-hidden="true">✓</span> No <code>!important</code></li>
</ul>
<div class="card__foot">
<span class="price">$24<small>/mo</small></span>
<button class="cta" type="button">Choose Ember</button>
</div>
</div>
</article>
</section>
<!-- ============ Donut scope ============ -->
<section class="donut-section" aria-labelledby="donut-h">
<div class="donut-head">
<div>
<h2 id="donut-h">Donut scope <code>(root) to (limit)</code></h2>
<p class="donut-copy">
The teal theme applies to the whole thread <em>except</em> everything inside the
quoted reply — the <code>to (.quote)</code> lower boundary carves out a hole.
</p>
</div>
<label class="toggle">
<input type="checkbox" id="donutToggle" checked />
<span class="toggle__track" aria-hidden="true"><span class="toggle__thumb"></span></span>
<span class="toggle__text">Donut hole</span>
</label>
</div>
<div class="thread" id="thread" data-hole="on">
<div class="note">
<p class="note__author">@ada · theme scoped</p>
<p class="note__body">This paragraph sits in the scoped root and picks up the teal accent and link color.</p>
<a class="note__link" href="#">In-scope link →</a>
<blockquote class="quote">
<p class="note__author">@quoted · outside the hole</p>
<p class="note__body">This quoted block is past the <code>to (.quote)</code> limit, so the scoped rules stop here and it keeps neutral styling.</p>
<a class="note__link" href="#">Un-scoped link →</a>
</blockquote>
<p class="note__body">Back in scope again after the quote — the accent returns.</p>
</div>
</div>
</section>
<!-- ============ Live source ============ -->
<section class="source" aria-labelledby="source-h">
<div class="source__bar">
<h2 id="source-h">Generated <code>@scope</code> rule</h2>
<span class="source__hint">updates as you drag the hue sliders</span>
</div>
<pre class="source__code"><code id="cssOut"></code></pre>
</section>
</main>
<script src="script.js"></script>
</body>
</html>This demo shows how the native CSS @scope at-rule confines styles and custom-property theme tokens to a subtree of the DOM, so two side-by-side product cards can share the exact same class names (.card, .badge, .cta) yet render with completely different brand palettes. Each region declares @scope (.brand-a) / @scope (.brand-b), and the tokens defined inside one scope never bleed into the other — no BEM prefixing, no CSS Modules, no !important arms race.
The third panel demonstrates donut scoping with the @scope (root) to (limit) syntax: styles apply to everything inside the outer boundary except the inner region past the lower limit, letting you theme a comment thread while leaving an embedded quote untouched. Interactive controls let you retune each brand’s accent hue live, toggle the donut hole on and off, and inspect the generated @scope block so you can see exactly what the browser is scoping.
It leans entirely on the platform @scope cascade feature. A @supports selector(:scope) feature detect drives a graceful fallback banner and attribute-scoped styling for browsers that haven’t shipped @scope yet, so the regions still theme correctly everywhere while clearly labeling when true native scoping is active.