Inheritance is availability, not an API
Shadow DOM scopes selectors and internal structure. Selected CSS values still cross the boundary through inheritance, including CSS custom properties. A value declared on :root may reach a component host and descendants in its shadow tree when they stay in the inheritance chain and no nearer declaration changes the result.
That behavior shows only that the value is available under the tested conditions. It doesn't mean the component owner promises to keep reading the name, permits consumers to override it, or guarantees that it affects every mode and state. Those are component API decisions.
Keep the claim narrow
Don't claim that every component sees every root property. A nearer semantic scope, a direct declaration on the host, a component mapping, or a different fallback path can change the result. Inspect the actual chain.
Separate four responsibilities
- Upstream semantic source: owns the meaning and light and dark values, such as the surface and content colors for a success status.
- Emitted runtime property: carries that decision into application CSS. Its presence is artifact evidence, not proof that a component consumes it.
- Public component property: gives consumers a documented, component-scoped override point such as
--chip-background. - Private internal alias: connects the public property to a named element inside the shadow tree. It remains an implementation detail even when its computed value can be inspected.
A component may also inherit an ordinary property such as color, expose a CSS part, or hard-code an internal rule. Each surface has a different contract. Calling all of them tokens hides who may change what.
Write the styling contract before the CSS
This contract describes the illustrative status chip used throughout the guide. It is a proposed record, not evidence from an executed fixture. Copy it into the component repository, then replace the example identities, owners, and policies with project data.
| Background property | Foreground property | Required record | |
|---|---|---|---|
| Upstream authority | Application design-system owner | Application design-system owner | Name the versioned source, not merely a team. |
| Semantic role | Success-status surface | Success-status content | Describe intent rather than a color name. |
| Emitted name | --app-status-success-bg | --app-status-success-fg | Confirm both names in the delivered stylesheet. |
| Component API name | --chip-background | --chip-foreground | Document these on my-status-chip. |
| Interface status | Public | Public | Allowed classifications are public, private, or accidental. |
| Host scope | my-status-chip | my-status-chip | Overrides must declare the public property directly on the host in this fixture. |
| Component default | var(--app-status-success-bg, #e8f5ec) | var(--app-status-success-fg, #184c2f) | The component owner controls these illustrative fallbacks. |
| Fallback owner | Component owner | Component owner | Changing a fallback is a component-contract change. |
| Allowed overrides | Any documented selector that matches the component host directly | Any documented selector that matches the component host directly | An ancestor-only declaration of the public property is not a supported override here. |
| Modes | Light and dark behind one semantic name | Light and dark behind one semantic name | The application owns mode selection and delivery. |
| States | Success default; warning maps to its own role | Success default; warning maps to its own role | Verify every supported state separately. |
| Named consumer | .chip background-color | .chip color | Name the internal element and consuming CSS property. |
| Compatibility policy | Breaking if removed or semantics change | Breaking if removed or semantics change | Record deprecation and replacement rules. |
| Evidence status | Expected, unexecuted | Expected, unexecuted | Change only after recording an observation. |
| Correction owner | Unassigned until the failing layer is identified | Unassigned until the failing layer is identified | Don't infer ownership from the visible symptom. |
An underscore is documentation, not access control
A name such as --_chip-bg signals private intent to maintainers, but CSS doesn't enforce the convention. Consumers may still declare that name outside the component. Privacy comes from the documented contract, compatibility policy, and an implementation that declares the alias beside its internal consumer.
Build one complete, explicitly unexecuted fixture
The inert code specimen below uses plain custom elements, an open shadow root for inspection, and CSS custom properties. Its markup delimiters are escaped so the publication adapter treats the specimen as data instead of executable page content. Decode the entities only in an isolated local test file. This isn't an executed Identity Forge integration or a browser benchmark.
<style>
:root {
--app-status-success-bg: #e8f5ec;
--app-status-success-fg: #184c2f;
--app-status-warning-bg: #fff4cc;
--app-status-warning-fg: #5f4300;
}
[data-theme="dark"] {
--app-status-success-bg: #173d2a;
--app-status-success-fg: #c7f2d7;
--app-status-warning-bg: #4a3700;
--app-status-warning-fg: #ffe8a3;
}
.review-scope {
--app-status-success-bg: #dceeff;
--app-status-success-fg: #123b63;
}
/* Supported application overrides match the host directly. */
.review-scope > my-status-chip.host-override {
--chip-background: #244f73;
--chip-foreground: #f4f9ff;
}
#direct-chip {
--chip-background: #3b245f;
--chip-foreground: #f7efff;
}
/* This ancestor-only public declaration is a precedence test,
not a supported override in this fixture. */
.ancestor-public-test {
--chip-background: #7a2e2e;
--chip-foreground: #fff4f4;
}
</style>
<section data-theme="light">
<my-status-chip id="default-chip">Ready</my-status-chip>
<div class="review-scope">
<my-status-chip id="scoped-chip">Scoped semantic values</my-status-chip>
<my-status-chip id="matched-host-chip" class="host-override">Matched host override</my-status-chip>
</div>
<div class="ancestor-public-test">
<my-status-chip id="ancestor-public-chip">Ancestor public declaration</my-status-chip>
</div>
<my-status-chip id="direct-chip">Direct override</my-status-chip>
<my-status-chip id="warning-chip" status="warning">Review</my-status-chip>
<status-cluster id="nested-cluster"></status-cluster>
</section>
<script>
class MyStatusChip extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host {
display: inline-block;
--chip-background: var(
--app-status-success-bg,
#e8f5ec
);
--chip-foreground: var(
--app-status-success-fg,
#184c2f
);
}
:host([status="warning"]) {
--chip-background: var(
--app-status-warning-bg,
#fff4cc
);
--chip-foreground: var(
--app-status-warning-fg,
#5f4300
);
}
.chip {
--_chip-bg: var(--chip-background, #e8f5ec);
--_chip-fg: var(--chip-foreground, #184c2f);
display: inline-flex;
align-items: center;
border-radius: 999px;
padding: 0.25rem 0.625rem;
background-color: var(--_chip-bg);
color: var(--_chip-fg);
}
</style>
<span class="chip" data-consumer="chip"><slot></slot></span>
`;
}
}
class StatusCluster extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: "open" });
root.innerHTML = `
<style>
:host {
display: block;
--app-status-success-bg: #edf0ff;
--app-status-success-fg: #292f6b;
}
</style>
<my-status-chip id="nested-chip">Nested</my-status-chip>
`;
}
}
customElements.define("my-status-chip", MyStatusChip);
customElements.define("status-cluster", StatusCluster);
</script>The background path starts with --app-status-success-bg in application CSS, passes through --chip-background on the host and --_chip-bg on .chip, then reaches background-color on that element. The foreground follows the same structure. The nested cluster supplies different semantic values in its shadow tree, while the nested chip consumes the same public component names.
The distinction between semantic scope and component override matters. .review-scope changes inherited application semantic properties, so the component's :host mapping sees different source values. By contrast, .ancestor-public-test only places --chip-background on an ancestor. The component declares the same public property on its own host, so the inherited ancestor value isn't the supported override path. .review-scope > my-status-chip.host-override and #direct-chip match the host itself and are the supported external overrides in this fixture.
Expected, not observed
The fixture is a reproducible test proposal. When you execute it, record the browser, operating system, component version, stylesheet identity, and actual computed values. Don't turn the expectations below into pass claims until those observations exist.
Test precedence with an expectation-first matrix
Run one condition at a time, resetting the fixture between cases. Compare normalized computed colors because a browser may serialize them differently from the notation in the stylesheet.
| Setup and expected trace | Inspect | Failure condition | |
|---|---|---|---|
| Document default | In light mode, the success semantic pair should reach the public pair, private pair, and .chip. | Stylesheet, host, internal alias, rendered foreground and background. | Any hop is missing or resolves to a different normalized color. |
| Container semantic scope | Place the chip in .review-scope; its nearer semantic pair should feed the same host mapping. | Container semantic values, host public values, internal aliases, rendered values. | The semantic source changes but a downstream hop remains on the document default. |
| Host-matching application override | Use .review-scope > my-status-chip.host-override so the rule declares both public properties directly on the host. | Matched selector, host public pair, internal pair, rendered pair, unaffected sibling. | The selector does not match, the target ignores the declaration, or a sibling changes. |
| Direct host ID override | Set both public properties on #direct-chip; they should replace the semantic mapping for that host only. | Host public pair, internal pair, rendered pair, unaffected sibling. | The target ignores the override or a sibling changes. |
| Ancestor public declaration | Place the chip inside .ancestor-public-test. Because the component declares the public pair on :host, the ancestor's inherited public values are not expected to win. | Ancestor value, host public value, internal alias, rendered value. | The case is documented as a supported override, or the observed result is accepted without reconciling the contract. |
| Nested component | Inspect the chip inside status-cluster; the cluster's semantic pair should cross the nested boundary. | Cluster host, nested chip host, nested .chip, rendered pair. | The value disappears at a boundary or changes an unrelated outer chip. |
| Dark mode | Change the containing data-theme value to dark; stable semantic names should resolve to the dark pair. | Mode selector, semantic source pair, host pair, internal pair, rendered pair. | Names change, one side remains light, or the component takes ownership of application mode selection. |
| Missing public mapping | Remove the :host public declaration and its upstream source in an isolated variant; the private alias fallback should supply its documented value. | Host public value, internal alias, rendered pair. | No value reaches the consumer or the result differs from the documented fallback. |
| Missing upstream source | Keep the :host mapping but omit the application semantic property; the nested var() fallback should resolve on the host. | Emitted stylesheet absence, host public value, internal value. | A stale or accidental ancestor value masks the missing source. |
| Invalid public value | Set --chip-background to not-a-color directly on the host. Record the custom-property text without predicting a valid rendered color. | Host text, internal text, computed background-color, test output. | The case is accepted without recording the consuming property's result and the product policy. |
| Fallback chain | Test source present, source absent, and public mapping absent as separate cases. | Every var() input and the final rendered property. | A fallback is credited without proving which earlier value was unavailable. |
| Private-alias declaration | Declare --_chip-bg outside the component; the declaration on .chip should remain the local source in this fixture. | External private-name value, .chip private-name value, rendered background. | The external declaration becomes a supported dependency or changes the consumer unexpectedly. |
| Protected surface | Declare an undocumented --chip-radius on the host; border-radius should remain the internal 999px rule. | Host declaration and computed border-radius on .chip. | An undocumented hook changes the protected rule or is adopted without a contract decision. |
An invalid custom-property value needs a separate case. Text such as not-a-color can remain visible as the computed value of a custom property, then fail when substituted into background-color. A var() fallback doesn't validate arbitrary color text. Observe the consuming property first. Then decide whether the component should constrain, reject, document, or tolerate the input.
Inspect every hop, not only the screenshot
Start with the emitted stylesheet. Confirm that the identified artifact contains the expected semantic names and mode values, then record its version, build identifier, content hash, or another stable identity. A correct source file doesn't prove the application loaded the same artifact.
- 1
Inspect the emitted property
Locate the exact semantic name for each mode in the delivered stylesheet or built CSS. Record the artifact identity and selector. If the property is absent here, stop before blaming the component.
- 2
Inspect the component host
Read the upstream semantic property and documented public property on the same host. This separates delivery and cascade behavior from the shadow-tree implementation.
const host = document.querySelector("#default-chip"); const read = (element, name) => getComputedStyle(element).getPropertyValue(name).trim(); console.table({ sourceBackground: read(host, "--app-status-success-bg"), sourceForeground: read(host, "--app-status-success-fg"), publicBackground: read(host, "--chip-background"), publicForeground: read(host, "--chip-foreground") }); - 3
Inspect the internal alias
For this open-shadow-root fixture, read the private aliases from the named internal consumer. If a production component has a closed shadow root, collect the evidence inside the component repository instead of weakening encapsulation for outside inspection.
const chip = host.shadowRoot.querySelector( '[data-consumer="chip"]' ); console.table({ privateBackground: read(chip, "--_chip-bg"), privateForeground: read(chip, "--_chip-fg") }); - 4
Inspect the rendered consumer
Read the CSS properties that consume the aliases. Record or normalize the browser's serialization instead of comparing it blindly with a hex literal.
const rendered = getComputedStyle(chip); console.table({ renderedBackground: rendered.backgroundColor, renderedForeground: rendered.color, renderedRadius: rendered.borderRadius }); - 5
Compare ancestor and host-matching overrides
Read the public property on both hosts. The ancestor-only case tests inheritance; the matched-host case tests the documented component API.
const ancestorCase = document.querySelector( "#ancestor-public-chip" ); const matchedHostCase = document.querySelector( "#matched-host-chip" ); console.table({ ancestorPublic: read( ancestorCase, "--chip-background" ), matchedHostPublic: read( matchedHostCase, "--chip-background" ) }); - 6
Inspect the nested path
Resolve the nested chip through both open shadow roots, then repeat the host, alias, and rendered checks. The observation applies only to the recorded fixture and condition.
const cluster = document.querySelector("#nested-cluster"); const nestedHost = cluster.shadowRoot.querySelector("#nested-chip"); const nestedConsumer = nestedHost.shadowRoot.querySelector( '[data-consumer="chip"]' ); console.table({ nestedSource: read(nestedHost, "--app-status-success-bg"), nestedPublic: read(nestedHost, "--chip-background"), nestedPrivate: read(nestedConsumer, "--_chip-bg"), nestedRendered: getComputedStyle(nestedConsumer).backgroundColor });
A screenshot is downstream evidence
A screenshot can show that a surface looked right under one condition. It can't identify the source artifact, the declaration that won the cascade, or whether the implementation used a private dependency. Keep the visual evidence, but pair it with the computed-value trace.
Start from named semantic roles
A Web Component contract is easier to maintain when its defaults map to stable semantic roles instead of raw colors. Inspect an existing token system before defining the host-facing properties.
Classify each styling surface by consumer need
| Use it when | Contract consequence | Do not assume | |
|---|---|---|---|
| Public component property | A consumer needs to change one documented component decision. | The name, semantics, host scope, modes, states, and compatibility policy belong to the component API. | Every internal value should be exposed. |
| Private alias | The implementation needs a readable local mapping or fallback chain. | The component may rename or remove it without consumer migration support. | An underscore prevents external declarations. |
| Accidentally inherited property | No owner has intentionally classified the dependency. | Block new adoption until it is made public, made private, or removed. | Current inheritance already creates a compatibility promise. |
| CSS part | A consumer needs controlled selector-based access to a named internal element. | The part name and permitted styling scope become documented customization surfaces. | A part is interchangeable with a semantic token. |
| Inherited ordinary property | Normal CSS inheritance, such as text color, is intentionally part of composition. | Document the supported context and any resets or exceptions. | Every ordinary property inherits. |
| Hard-coded internal rule | The component owns a protected decision with no current consumer need for variation. | Changes stay with the component implementation and its regression evidence. | Consumers can or should override it. |
DÁP publishes component custom properties and CSS parts instead of asking consumers to discover internals. Another system may choose different surfaces. What matters is whether each surface is deliberately supported.
Keep mode ownership outside the component
Stable semantic names let light and dark values change without making the component choose the theme. In the fixture, the application changes values behind --app-status-success-bg and --app-status-success-fg. The component keeps mapping them into --chip-background and --chip-foreground.
The consuming application still owns preference detection, persistence, selector strategy, stylesheet loading, and import order. A component can support both modes while staying neutral about how the application selects one. Record nested mode scopes separately because a local theme container may legitimately differ from the document.
Test foreground and background together in every supported state. Checking only the background may leave content on the wrong mode value. This procedure verifies delivery and consumption, but it doesn't establish an accessibility result by itself.
Copy the acceptance record and fill observations last
Create one record per condition when values or ownership differ. This example is intentionally incomplete. Its disposition remains block because no runtime observation has been recorded.
source:
authority: "Application design-system owner"
version: "REPLACE_WITH_SOURCE_VERSION"
semantic_role: "success status background and foreground"
artifact:
identity: "REPLACE_WITH_STYLESHEET_BUILD_OR_HASH"
selector: ":root and recorded mode selector"
emitted_background: "--app-status-success-bg"
emitted_foreground: "--app-status-success-fg"
component:
name: "my-status-chip"
version: "REPLACE_WITH_COMPONENT_VERSION"
public_background: "--chip-background"
public_foreground: "--chip-foreground"
supported_override_scope: "selector matching component host directly"
inherited_public_override_supported: false
private_background: "--_chip-bg"
private_foreground: "--_chip-fg"
consumer: ".chip"
consumer_properties:
- "background-color"
- "color"
test:
condition: "document default, light mode, success state"
browser_and_os: "RECORD_AT_EXECUTION"
expected_host_background: "#e8f5ec"
expected_host_foreground: "#184c2f"
expected_internal_background: "#e8f5ec"
expected_internal_foreground: "#184c2f"
expected_rendered_state: "success pair on .chip"
observation:
emitted_values: "NOT_RECORDED"
host_background: "NOT_RECORDED"
host_foreground: "NOT_RECORDED"
internal_background: "NOT_RECORDED"
internal_foreground: "NOT_RECORDED"
rendered_background: "NOT_RECORDED"
rendered_foreground: "NOT_RECORDED"
protected_surface_result: "NOT_RECORDED"
evidence_reference: "NOT_RECORDED"
exception:
status: "none proposed"
rationale: ""
expiry_or_review_trigger: ""
correction:
suspected_layer: "unassigned"
owner: "unassigned until evidence identifies the layer"
next_inspection: "run the recorded condition"
disposition:
value: "block"
reason: "Expected values exist, but runtime observations are absent."Use accept when the identified artifact and representative condition match the contract and protected surfaces remain unchanged. Use revise when the intended contract remains valid but needs a bounded correction or documented exception. Use block when required evidence is absent, authority is unresolved, an unsupported dependency is being adopted, or the result contradicts a release requirement.
Route failures from evidence, not appearance
A browser mismatch doesn't automatically belong to delivery, and a difference from a design file doesn't automatically belong to transformation. Follow the trace until you find the first recorded divergence. Only then should you assign the correction.
| Next inspection | Layer that may own correction | Evidence required before assignment | |
|---|---|---|---|
| Source role or mode value is absent | Open the versioned upstream source and confirm the approved role. | Upstream source owner. | Source identity, expected role, approval state, and actual source value. |
| Source is correct but emitted property is absent or stale | Inspect transformation configuration and generated stylesheet identity. | Transformation or build owner. | Matching source version, build input, configuration, and emitted artifact. |
| Artifact is correct but the host lacks the semantic value | Confirm stylesheet loading, selector match, mode scope, and import order. | Delivery or application owner. | Loaded artifact identity and computed semantic value on the host. |
| Host semantic value is correct but public property differs | Inspect direct host rules, matched external selectors, :host mappings, and precedence. | Application cascade or component-contract owner. | Matched declarations and computed values on the same host. |
| Ancestor public declaration does not override | Check whether the public property was declared only on an ancestor rather than by a selector matching the host. | Consumer implementation owner unless inherited overrides are added to the contract. | Ancestor declaration, host declaration, computed host value, and documented override scope. |
| Public property is correct but private alias differs | Inspect the internal alias declaration and active state selector. | Component implementation owner. | Component version, active state, public value, and internal computed value. |
| Private alias is correct but rendered property differs | Inspect the consuming declaration, competing internal rules, and value validity. | Component consumer implementation owner. | Alias value, matched rule, rendered property, and active state. |
| Nested component alone differs | Trace the value at the outer host, nested host, and nested consumer. | Nested scope, component mapping, or application composition owner. | Separate computed values at every boundary. |
| External private alias changes the result | Check where the component declares the alias and whether anyone documented the external dependency. | Component owner for implementation; consumer owner for unsupported use. | Matched declarations and the contract classification. |
| Protected surface changes | Identify the selector, part, inherited value, or custom property that reached it. | Component-contract owner after reachability is confirmed. | Reproduction, matched rule, documented API list, and affected consumers. |
Where Identity Forge stops
Identity Forge can provide upstream semantic decisions and CSS or DTCG token exports. Those artifacts can supply the application side of this chain. They don't define a project's Web Components, select its public styling properties, control its theme selector or import order, or prove what a browser rendered.
The consuming project owns the mapping from exported roles to runtime properties, along with the Web Component styling API, internal aliases, selectors, fallbacks, compatibility policy, fixtures, and acceptance evidence. Calling an artifact a DTCG export also doesn't establish compatibility with every transformer or consumer.
Make one inherited property intentional
Choose one custom property that currently reaches a shadow tree. Classify it as public, private, or accidental. If it's public, document the selector scope that may override it and the compatibility policy. If it's private, remove consumer reliance and declare it beside the internal element where practical. If it's accidental, block new adoption until an owner decides its status.
Next, run one recorded condition covering a mode, state, nested scope, host-matching override, ancestor-only declaration, and protected surface. Fill the observation fields and evidence reference before changing the disposition from block. Finish that record before another component or application adopts the name.
Sources
- How I Use ShadowDOM with Web Components in Design Systems: Shows system and theme tokens feeding public component custom properties, with shorter underscore-prefixed aliases treated as private by naming convention and backed by fallback chains.
- The Value of Design Tokens in Modern Web Development: Describes primitive, semantic, and component token tiers and shows CSS custom properties being consumed inside a Web Component shadow tree.
- How Nordhealth uses Custom Properties in Web Components: Provides a production design-system case study distinguishing globally consumed tokens from selected component-level custom properties and explains why component overrides are applied directly to the host.
- Styling, Customizing - DÁP Szolgáltatások: Documents prefixed design tokens, component custom properties, and CSS parts as explicit customization surfaces in a Web Component design system.
- 8 Ways to Style the Shadow DOM: Explains inherited ordinary properties and CSS custom properties as mechanisms that can carry values across a shadow boundary.
- Identity Forge: Documents Identity Forge design kits and implementation artifacts, including CSS and DTCG token exports that can serve as upstream inputs to a project-owned Web Component contract.