Extract the brand, do not override it
A graft that ships its own colours looks like a foreign object in every host; one that reads a documented set of custom properties, with your own values as fallbacks, inherits the brand without asking anyone to adopt your design system.
The review gate renders inside its shadow root and nothing of the host’s reaches in. That is the thing the Web Components module bought you, and the first time you see it working in a real product you will want to give some of it back. Your component is grey and blue in a product that is maroon and cream. It is set in your type scale, at your radius, with your focus ring, in the middle of a page that agreed to none of it.
Nothing is broken. No test failed. It just looks like something somebody bolted on, which is the impression you least want to make on the team deciding whether to keep it. And the fix that comes to mind first — shipping a theming API and asking them to fill it in — is the fix that does not scale, because it converts every one of ninety integrations into a design conversation with a team that has no design budget for you.
The leak you keep on purpose
The cascade lesson established that inheritance crosses every boundary this course has, and that custom properties are the most stubborn case. Three facts stack to make that true, and it is worth having all three in one place, because this lesson is built entirely on them.
First, custom properties inherit. MDN states it without qualification: a custom property defined with two dashes “always inherits the value of its parent”, the exception being a property registered through @property with inherits: false.
Second, inheritance is computed over the flattened tree, which includes shadow content — CSS Scoping Module Level 1 grounds this in the rule that slotted content inherits from the slot it is assigned to rather than from its light-DOM parent. So a custom property declared anywhere above your element is visible inside your shadow root with no plumbing at all.
Third, the one blunt tool for severing inheritance does not touch them. MDN says all resets every property “except unicode-bidi, direction, and CSS Custom Properties.”
Read together, those three say something useful: the host’s brand is already inside your component. You do not have to build a channel for it. You have to decide which values to read, and what to do when they are not there.
Read their names, publish yours
The mistake that looks like the obvious design is to have your internals read the host’s names directly: color: var(--brand-ink, #1a1a1a) in every rule that needs text colour. It works in the host you tested. At host number two the token is called --color-text-primary, and now you are editing fifty declarations, in a bundle that was supposed to be one file.
Give the component exactly one vocabulary, namespaced with the prefix you fixed in the cascade lesson, and never let a host name appear inside it.
/* inside the shadow root — the only names the component knows */
:host {
--graft-text: #1a1a1a;
--graft-surface: #ffffff;
--graft-accent: #2b6cb0;
}
.gate__approve {
color: var(--graft-text);
background: var(--graft-accent);
}Then write one small stylesheet per host whose entire job is translation. It reads their names and assigns yours, and because custom properties inherit, declaring it on your own element is enough to reach every rule inside the shadow root.
/* host-acme.css — the only file that differs between hosts */
agent-review-gate {
--graft-text: var(--brand-ink, #1a1a1a);
--graft-surface: var(--brand-paper, #ffffff);
--graft-accent: var(--brand-action, #2b6cb0);
}One indirection, and the shape of the problem changes. The bundle is identical everywhere. The per-host artifact is a mapping of about a dozen lines, which is small enough to write by hand and, as the next lesson shows, small enough to generate. Declaring it on your element selector rather than on the host’s :root also keeps your promise from the graft contract: you named the globals you occupy, and adding names to their document root was not on the list.
The fallback does not mean “if they forgot”
Every mapping line above has a fallback, and it is worth being precise about when it fires, because the loose version of the rule will mislead you at exactly the wrong moment. MDN’s rule is that the fallback value is used when the referenced custom property is invalid — not when the host is generally unbranded, not when the value looks wrong to you. A property that was never declared, or never inherited into scope, is the practically common case. It is not the whole rule.
The second detail is a parsing one, and it saves an afternoon: var() takes everything after the first comma as the fallback. MDN’s own worked example is var(--foo, red, blue), whose fallback is red, blue — which is what makes a comma-bearing fallback work without any escaping.
/* fallback is the whole of "Söhne, system-ui, sans-serif" */
font-family: var(--brand-sans, Söhne, system-ui, sans-serif);
/* two-level: their name, your namespaced default, in that order */
color: var(--brand-ink, var(--graft-text));Verify the fallback rather than reasoning about it. Open the host, delete their declaration in the devtools style panel, and watch what your component does. That takes fifteen seconds and it is the only way to find out that their token was declared on a wrapper you are rendering outside of, which is a real failure and looks nothing like a missing definition.
Where people get burned
Reading a host token you did not agree on is an undocumented dependency. If their platform team renames --brand-ink in a refactor, your component turns to its fallback in production and nobody on either side has a test that catches it. The names you read go in the graft contract, in the section that lists what you need back from the host team, phrased as a request: these are the four names we depend on, tell us before you rename them. That converts a silent failure into a conversation you get to have first.
When the host has no tokens at all
Plenty of them will not have any. A 2018 stylesheet has hex values in fifteen thousand lines and no variables, and there is nothing to read.
The answer is not to give up and ship your palette. It is to do by hand what the host never did: open their product, measure the computed values the cascade lesson already had you record, and pick the smallest set that makes your component look native. In practice that is a text colour, a surface colour, a border colour, an accent, and the type stack — five values, not fifty. Write them as literals in that host’s mapping file, note in the mapping that they were measured on a date rather than read from a token, and offer the host team the same five names as tokens they could adopt later if they ever want the component to follow a rebrand automatically.
Resist one specific shortcut. :host-context() looks like a way to detect what the host is and theme accordingly, and it is a trap: MDN records that it was removed from the specification “due to opposition from vendors, performance considerations, and interest in alternatives”, with compatibility data showing Firefox and Safari unsupported (checked 2026-09-03). A mapping file works in every engine and needs no detection.
There is also no shortcut coming from the format work. The Design Tokens Format Module standardises the file — reserved keys like $value and $type, aliasing, and a list of token types — and says nothing about what a token should be called. Two hosts can both be perfectly conformant and share not one name. Somebody has to look at each host and write the mapping. The next lesson is about making that somebody produce ninety files from one source instead of ninety files from ninety afternoons.
Check your recall
Answer from memory — no scrolling back.
Retrieval check
Your bundle is one file for every host. Name the one artifact that must differ per host, say where it is declared, and say why it cannot live inside the component.
Check your answer
A mapping stylesheet: a dozen declarations that read the host’s own token names and assign your namespaced ones, declared on your element selector so it inherits into the shadow root without adding names to the host’s :root.
It cannot live inside the component because its content is a set of claims about a specific host — what they call their text colour, their surface, their accent — and those claims are the one thing that genuinely varies across ninety products. Putting them inside the bundle would mean either ninety bundles or a component that knows every host’s vocabulary. Keeping them outside means the bundle is identical everywhere and the variable part is small, reviewable, and cheap to regenerate when a host rebrands.
Hands on
Write the brand mapping for one real host
Done when: ARTIFACT.md carries a brand mapping for one real host: every host token name quoted from their live product, your namespaced name for each, a fallback for each, and a note for every value that was measured rather than read — with each fallback verified by deleting the host declaration in devtools, not by reasoning about it.
- Open the host you have been surveying since the host-reading lesson. In the console, list what they actually publish:
getComputedStyle(document.documentElement)and read the Computed panel with “Show all” on, or walk their:rootrule. Write down every custom property name they define, not just the ones you want. - Pick the five your component actually needs: text, surface, border, accent, and the type stack. If a name does not exist, mark that value measured and record the computed value with the date.
- Write the mapping file. Their name on the right, your prefixed name on the left, a literal fallback on every line. Declare it on your element selector, never on
:root. - Verify each fallback. In devtools, delete the host’s declaration and confirm your component falls back to your value rather than to nothing. Any line that does not behave is a line where their token was declared somewhere you do not inherit from — find where, and fix the mapping rather than the fallback.
- Save the mapping into
learning/grafting-ui/ARTIFACT.mdunder the host-brand checkpoint, and add the list of host token names you now depend on to the graft contract, in the section naming what you need back from the host team. - Bring the mapping into the chat. I will push back on any value you wrote down without saying whether it was read or measured.
What this does not cover
One host, one mapping, written by hand. That is the right amount of machinery for one host and the wrong amount for ninety, and the failure is not effort but drift: ninety hand-written files diverge in their defaults, their names and their coverage until nobody can say what the component looks like anywhere. The token-source lesson takes the same mapping and makes it an output of a build, along with the honest status of the format that makes that possible.
This lesson also only inherited what is visible. The host’s identity — who the user is, and whether your component can find out without asking them to log in twice — is a different inheritance with a different mechanism, and it decides more integrations than colour does. That is the session lesson.
Read this next — primary source
Using CSS custom properties (variables)MDN Web Docs — free. Fetched 2026-09-05
This lesson takes two rules from it: that a custom property declared with two dashes always inherits from its parent, and that the fallback in var() fires when the reference is invalid rather than whenever the host merely forgot to define something. The full page adds the parts a graft hits later — how a custom property is validated at all, what happens when a value is invalid at computed-value time, and the JavaScript API for reading and writing them at runtime. Read it once end to end, because every theming decision in this module rests on the inheritance rule and most people carry a slightly wrong version of it.
Stuck, curious, or think this lesson is wrong? Ask your teaching agent. The lessons are the scaffold; the conversation is where the learning gets unstuck.