What broke when I ran a design-inheritance engine against 20 real sites
I am building a testimonial embed whose only difference from its competitors is that it matches the host page’s design with no configuration. So the whole product rests on one question, and I measured it before building the product.
The question, and the bar I set
On how many real sites does a widget render correctly with zero configuration? Not “does the code run”, and not “does it look fine on my own test page”. Twenty sites I did not build, in four groups of five, scored against criteria written down before the run.
I set the bar at 16 of 20, and a second condition that mattered more to me: no site may render badly. A widget that gives up and looks neutral is a miss I can live with. One that puts a white card on a black page is the exact thing the product exists to avoid.
The result
| Axis | Pass | Graceful miss | Broken |
|---|---|---|---|
| Chromium, light | 17 | 3 | 0 |
| Chromium, prefers-color-scheme: dark | 17 | 3 | 0 |
| WebKit (Safari 16+), light | 16 | 4 | 0 |
Bar met on all three axes, and nothing rendered badly on any of them. The group I expected to fail did not, and the group that hurt is the one I am selling to. More on both below.
The pass count is the least interesting number
A pass means the render is correct, not that detection was strong. Three other numbers say more, and collapsing them into one is how my first attempt at this overstated itself.
| Measure | Chromium | What it means |
|---|---|---|
| Sites passing the bar | 17 / 20 | The rendered result is right |
| Every core value read from the page | 12 / 20 | Background, text and font all read, none defaulted |
| Accent is the page's real brand colour | 14 / 20 | The rest took a neutral the page also uses |
| Any CSS custom property matched | 8 / 20 | The fast path fired at all |
That last row moved where the engineering goes. My spec treats CSS custom properties as the primary path. On real sites they fire on 8 of 20 and are sufficient on exactly one, ui.shadcn.com, which is where the whole --background and --foreground convention comes from. Semantic colour tokens are an application convention. Marketing sites built in Framer and Webflow ship nothing like them. Reading computed styles off the page’s own elements is the path that carries the product.
Two criteria are weaker than their names suggest
Both were found by reviewing my own writeup, and both hold up the headline.
The font rule accepts any face the page uses for text, not the face the page sets prose in. Three of the seventeen passes lean on that branch rather than on matching the page’s actual prose font. Remove it and I am at 15 of 20, under my own bar. Two of the three look like limits of my measurement, where the prose sample caught a monospace block on webflow.com and a headline face on theguardian.com. The third is a real miss: rive.app renders Tomorrow where the page sets prose in Inter Display. So read the result as depending on a deliberately permissive font rule.
The accent criterion is a floor, not a gate. It passes any unsaturated accent as long as the page’s palette contains a neutral, and all twenty palettes do. Since the engine falls the accent back to the page’s foreground when detection finds nothing, total accent failure auto-passes. The number to read is the 14 of 20 above, not the pass count.
The comparison I had to retract
The first version of my writeup compared 14 of 20 before my fixes against 17 of 20 after, and called the improvement auditable. That comparison was junk. I had changed the scoring rules between the two runs, so it credited a scoring change to the engine.
Re-scoring the archived observations with the current scorer is the only honest form of the claim. Done that way, the fixes cost two passes on Chromium and removed three of the four broken states on WebKit. That is the trade I would have made anyway. It is not what I first claimed.
There is a general version worth carrying: if you set yourself a numeric gate, the easiest way to pass it is to move the ruler without noticing.
Six things that surprised me
lab() is how Chrome reports computed colours
Chrome serialises computed values in lab() whenever the author wrote oklch(), lab() or color-mix(). So getComputedStyle(html).backgroundColor returns lab(1.90334 0.278696 -5.48866) on tailwindcss.com. My parser had lab() on a list of syntaxes I skipped as exotic.
Three real, opaque page backgrounds therefore read as nothing declared. That pushed the engine onto an assumed white canvas, and from there onto light cards on dark pages. Fixing it raised core-slot detection from 10 of 20 to 12. One detail to get right if you do this: CSS lab() is defined against the D50 white point rather than sRGB’s D65, so the conversion has to adapt between them. Skip that step and every grey comes out tinted.
Safari has its own name for “no font declared”
-webkit-standard is Safari’s equivalent of Chrome’s Times New Roman: what it computes for an element with no font. My rejection list for browser defaults was written from Chromium and jsdom evidence only, so Safari’s name for the same state passed straight through.
On theguardian.com and bbc.co.uk the widget rendered testimonials in Times while reporting a successful font detection. Three of the four WebKit broken states were this one bug. The lesson is about the shape of the rule rather than the missing string: a per-engine denylist silently stops working on any engine nobody tested.
False positives are the real risk, not failures
A page that declares nothing still returns a computed color: rgb(0,0,0) and a font name. Accept those and you confidently render testimonials in black Times and report success. Detection now needs corroboration from another value before it trusts a browser default.
The trade-off is deliberate. A site genuinely using Times New Roman is now misread as having no font and gets a neutral default. Falling back on a real signal is much cheaper than adopting a fake one.
CSS-in-JS was not the problem. Webflow and Framer were
I expected hashed class names from styled-components and Emotion to leave nothing to sample, and I said so in writing before the run. All five passed on Chromium. A hashed selector computes to the same concrete color and font-family as a hand-written one, because getComputedStyle does not care what the selector was called. Obfuscation hides the CSS you wrote, not the pixels.
The group that actually hurt is Webflow and Framer, where 2 of 5 miss. That is my target market, which is the least convenient possible place for a weakness.
A quarter of real sites refuse the script outright
A real cross-origin <script src> over https was refused by Content Security Policy on 5 of the 20 sites: news.ycombinator.com, en.wikipedia.org, sive.rs, attio.com and rive.app. An earlier three-site spike concluded CSP was not a barrier, which is what three sites will do to you.
This is a product problem rather than a code problem. Those customers have to add an origin to their policy before the embed runs at all, and the default failure mode is the worst available one: they see nothing and conclude the product is broken. A related trap cost me a day. An http script on an https page is blocked as mixed content before CSP is ever consulted, so my first CSP test was measuring something else entirely, and the console said so if I had read it.
A :host declaration loses to the page's own reset
One site passed every colour criterion and still looked bolted on, because the widget spanned the viewport and butted against a full-bleed black footer. My harness scored eight things about how the widget looked and nothing about where it sat, so nothing in it could say so.
Fixing that turned up something I did not know. For a shadow host element the outer tree wins: a :host declaration sorts below anything the page says about that element, whatever the specificity. patreon.com ships the usual * { margin: 0; padding: 0 }, so placement rules asking for 24px computed to zero. Important declarations reverse that ordering. The failure mode is the nasty kind, a feature that works everywhere except on the sites that need it, and a widget with no placement test would have shipped it.
Two smaller ones from the same chase. html { font-size: 62.5% } is not a page’s reading size, so deriving spacing from documentElement gave patreon.com an 8px root and the minimum spacing, which is the opposite of what a page that size needs. And a paragraph measure is the wrong width for a wall of cards: sizing to the prose column produced a 512px widget on a site whose sections run to 1152px, as foreign-looking as the edge-to-edge version and in the opposite direction.
The twenty sites
Chosen before the run, five per group, with a written reason for each. Public pages, no cooperation from anyone, and the harness detects each page’s stack independently of the label I gave it. That caught two of my own mislabels: cal.com is Framer-built rather than Tailwind, and patreon.com uses styled-components rather than Emotion.
| Group | Sites |
|---|---|
| Tailwind and shadcn | tailwindcss.com, ui.shadcn.com, supabase.com, cal.com, dub.co |
| Hand-written CSS | news.ycombinator.com, en.wikipedia.org, sive.rs, simonwillison.net, danluu.com |
| Webflow and Framer | webflow.com, framer.com, attio.com, lattice.com, rive.app |
| CSS-in-JS | styled-components.com, emotion.sh, theguardian.com, bbc.co.uk, patreon.com |
Try it on your own site
The demo takes any URL, loads that page in headless Chromium, injects this same engine and shows you every value it resolved and where each one came from. No account, and no email needed to see the result. It is a server-side render screenshotted for you rather than a live widget in your browser, which is the honest description of where this is.
Run it against your site. If it gets yours wrong I would like to know which one, because those are the interesting ones.