The graft contract
A one-page contract naming your tag, your events, your bundle budget and your breaking-change policy — and the four things you need back from the host team — is what turns a demo into something a platform team will let into production.
The mechanism is decided. Now a staff engineer at a portfolio company you met last week has to agree to put your code inside a product they get paged for at three in the morning. They do not know you. They have been handed integrations before, and the ones they remember are the ones that broke.
The instinct is to send them the component and offer to help. That produces an open-ended relationship with no boundaries, in which every future question about your surface arrives on their pager first. What gets accepted instead is a document: one page, naming exactly what you occupy, what you promise, and what changes without warning. Not because anyone enjoys documents, but because the actual thing being asked for is a bounded liability, and a bounded liability has to be written down to exist.
Inverting who writes it
Ian Robinson’s consumer-driven contracts (2006) makes an inversion worth stealing. A provider contract is normally declared by the provider — here is my interface, conform to it. Robinson’s version is assembled from what consumers actually need, which makes it “non-authoritative because derived from the union of existing consumer expectations,” and its stability “valid for a particular set of consumer contracts.”
Robinson is writing about services, not UI, and nothing in the article addresses grafting a component into a host page. But the inversion transfers exactly. You are the provider. Ninety hosts are the consumers. If you declare your contract from your own side, you will promise the things that are easy for you and stay silent on the things that are expensive for them — and every one of those silences becomes a support conversation. Assembled from the host’s side, the contract answers the questions they were going to ask anyway, and it gets shorter rather than longer as more hosts adopt it, because the union of what everyone actually needs is smaller than the union of what you could offer.
The five things you promise
One: the namespace you occupy, exhaustively. Every global you touch, listed. The custom element tag names. The CSS custom property prefix you fixed in the cascade lesson. Any window property. Any localStorage key. Any event name. A platform engineer is scanning for collisions with something they already have, and a list they can grep against is worth more than a paragraph of assurance.
Two: the events you emit, and their exact shape. This is the host’s only integration surface and it is where the shadow boundary will bite you if you have not thought about it. A CustomEvent fired from inside a shadow root is invisible outside it unless you set both flags: MDN gives bubbles and composed defaults of false, and composed is what “indicates whether or not the event will propagate across the shadow DOM boundary into the standard DOM”. Promise the events in the contract and you have promised both flags, whether or not you knew it.
this.dispatchEvent(
new CustomEvent('agent-review:approved', {
detail: { runId, fields },
bubbles: true,
composed: true
})
)Three: the bundle budget, as a number with a date. Not “small.” A gzipped byte count they can put in a budget check, plus what it becomes if they externalise the framework. Give them the number and you have given them the ability to catch you regressing it, which is the point.
Four: the browser floor you support, with the reason. The delivery-constraints lesson pinned two dates worth restating: ElementInternals and adoptedStyleSheets both reached Baseline widely-available status in March 2023 per MDN (checked 2026-09-03). If your floor is above theirs, say so in the contract rather than discovering it in their bug tracker. Baseline is the right vocabulary for this line, because “newly available” and “widely available” mean specific things — all four core browser engines, and thirty months after that — and a host team can check your claim without taking your word for it.
Five: your breaking-change policy. Semantic Versioning 2.0.0 gives you the vocabulary — MAJOR for “incompatible API changes,” MINOR for functionality added “in a backward compatible manner,” PATCH for “backward compatible bug fixes.” The work is saying what counts as your API, because for a grafted UI surface it is broader than a function signature. Renaming a CSS part is breaking. Removing an event is breaking. Changing the default visual density is arguably breaking, and you should decide which side of that line you are on before a host finds out for you.
Where people get burned
The promise most often made and most often broken is “we’ll never change your DOM.” It is broken not by carelessness but by ambition: the day someone adds a portal, a tooltip, a focus trap or a modal, your component reaches outside its own subtree. Decide now whether you are allowed to, write the answer in the contract, and if the answer is yes, name exactly where — “one container appended to document.body, id agent-graft-overlay-root.” A named exception is accepted. A violated absolute is remembered.
The four things you ask for
A contract with only obligations on one side is a wish. Ask for exactly four things, because four fits in a meeting:
- A CSP amendment, pre-drafted. Do not ask them to “allow” you. Send the exact directive change — the origin to add to
script-src, the origin to add toconnect-src, and nothing else. MDN’s CSP guide documents report-only mode, which is what you offer them as the safe way to try it. A narrow, drafted, reversible ask gets approved; a general one gets scheduled. - A named mount point. One element, one id, in their layout, that they own. This is the difference between a graft that can be moved by the host team without talking to you and one where every placement change is a ticket in your queue.
- The brand values, as custom properties. Not a Figma file, not a colour palette in Slack — the actual custom property names their application already sets. You will consume them with your own values as fallbacks. That request costs them nothing because the values already exist, and it is what makes the graft look native without either team adopting the other’s design system.
- One name and one channel. The person who owns the integration on their side, and where a break gets reported. Without this, the reporting path for a bug in your component is their customer support queue, and you will hear about it in week three.
What the contract is actually buying
Accessibility is worth a specific line, and it should be honest rather than reassuring. WCAG 2.2 became a W3C Recommendation in December 2024, with conformance levels A, AA and AAA. Portfolio companies with enterprise or public-sector customers have contractual obligations against it, and the moment your surface is inside their product it is inside their audit scope.
So state your conformance target, and state the gap. There is at least one you cannot close today: ARIA references are ID references, and MDN is explicit that “attribute element id references are only in-scope for target elements declared in the same DOM or shadow DOM as the element”. A host label cannot point at a control inside your shadow root. That is a real limitation with real consequences, it has its own lesson in the Web Components module, and a contract that quietly omits it is a contract that will be quoted back at you during an accessibility audit.
Retrieval check
Why does a contract assembled from the host’s questions end up shorter than one you design from your own side?
Check your answer
Because a provider-designed contract enumerates capability and a consumer-derived one enumerates need, and need is a much smaller set. Left to yourself you will document every prop, every slot, every theming hook — the things that are cheap for you to promise — and stay silent on install footprint, event flags, bundle size and breaking-change policy, which are expensive. Those silences are exactly what the host was going to ask about, so every one of them becomes a conversation you have ninety times.
Robinson’s framing is that the contract is “non-authoritative because derived from the union of existing consumer expectations.” The union of what ninety hosts actually need converges fast — they all want to know the same handful of things — whereas the union of what you could offer grows with every feature you ship. That is why the document gets shorter as adoption grows, and it is the strongest argument for writing it after reading a real host rather than before.
Hands on
Write the one-page contract for a host that has not agreed to anything yet
Done when: ARTIFACT.md carries a graft contract that fits on one page, covering all five promises and all four asks, in which the CSP ask is a pre-drafted directive change specific to the host you surveyed and the accessibility section names at least one gap you cannot currently close.
- Open the seam map, the constraint sheet and the delivery decision you have already written. Everything in the contract should be derivable from those three; anything you have to invent now is a gap in the survey, and it is cheaper to notice it here than in review.
- Write the namespace section first and make it exhaustive: tag names, the custom property prefix, any
windowor storage key, every event name. Then re-read it as a hostile reader looking for a collision with something they already ship. - For each event, write the literal dispatch options alongside it. Every event crossing a shadow boundary needs
bubbles: trueandcomposed: true— both default tofalse, so the contract line and the code line have to agree or you have promised something you do not emit. - Draft the CSP amendment as a diff against the actual policy string you captured from the host, not as a description. Two lines at most. Offer report-only as the first step.
- Write the accessibility section last and put the gap in it. If you are shipping inside a shadow root, the host cannot label your controls with their own elements — say so, in one sentence, with the consequence.
- Bring the contract into the chat. I will read it as the platform engineer who has to sign it, and I will look first for the thing you left out because it was awkward.
What this does not cover
Everything up to here has been survey and negotiation, which is the half of this work nobody teaches and the half that decides whether the other half ever ships. None of it has produced running code, and the contract has now committed you to a set of behaviours you have not yet built.
The Web Components module starts building them, in the order the contract makes expensive to get wrong: the custom element and its shadow root first, then what the boundary costs you in focus, form participation and event retargeting, then the accessibility gap this lesson made you write down — the one where ID references do not cross tree boundaries and the proposed fix is not shipping yet. The module closes on wrapping a React tree as a custom element without shipping React twice into a host that already has it, which is the first point where your bundle-budget promise becomes a real constraint rather than a number in a document.
Read this next — primary source
Consumer-Driven Contracts: A Service Evolution PatternIan Robinson, martinfowler.com, 12 June 2006 — free, long (about 45 minutes)
This lesson borrows one inversion from it — that the contract is derived from what consumers actually need rather than declared by the provider — and applies it to a UI graft, which Robinson never discusses. What the full article adds is the machinery for the case you will hit at host number twelve: how a provider contract stays stable while the set of consumers changes, what happens when two consumers want incompatible things, and why a provider contract assembled from real consumer expectations is smaller and more evolvable than one designed up front. That is the ninety-host problem, worked out twenty years ago for services.
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.