All posts
TutorialsSoraiaJul 18, 20268 min read

How to Extract Design Tokens from Any Website in Python

Extract colors, fonts, spacing, and shadows from any website with Playwright in Python - working code, the gotchas that bite, and the one-API-call shortcut.

The fastest way to extract design tokens from a website in Python is Playwright plus getComputedStyle(): load the page in a real browser, collect the computed styles of every element, and count what repeats. The counting is the important part. A site's design system is whatever it uses everywhere, not every value that appears once in a forgotten footer.

Parsing CSS files without a browser is the approach most tutorials suggest, and it is the one that works worst on modern sites. More on that below.

Extract colors with Playwright

First, install Playwright and a browser:

pip install playwright
playwright install chromium

Then collect styles from every element and count them:

from collections import Counter
from playwright.sync_api import sync_playwright

COLLECT = """
() => {
  const els = [...document.querySelectorAll('body *')].slice(0, 5000);
  const out = { colors: [], fonts: [], sizes: [], radii: [], shadows: [] };
  for (const el of els) {
    const s = getComputedStyle(el);
    out.colors.push(s.color, s.backgroundColor, s.borderTopColor);
    out.fonts.push(s.fontFamily);
    out.sizes.push(s.fontSize);
    out.radii.push(s.borderTopLeftRadius);
    out.shadows.push(s.boxShadow);
  }
  return out;
}
"""

def top(values, n=8, skip=("", "none", "normal", "auto", "0px", "rgba(0, 0, 0, 0)")):
    return Counter(v for v in values if v not in skip).most_common(n)

with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page(viewport={"width": 1440, "height": 900})
    page.goto("https://stripe.com", wait_until="networkidle")
    raw = page.evaluate(COLLECT)
    browser.close()

for name in ("colors", "fonts", "sizes", "radii", "shadows"):
    print(f"\n== {name} ==")
    for value, count in top(raw[name]):
        print(f"{count:>5}  {value}")

Run it and you get something like:

== colors ==
 3021  rgb(255, 255, 255)
 1204  rgb(26, 27, 37)
  412  rgb(99, 91, 255)
  118  rgb(66, 84, 102)

That third line is the brand color, found by nothing smarter than counting. getComputedStyle has already done the hard work for you: it resolved every CSS variable chain, every var(--a, var(--b)) fallback, every cascade conflict, and turned Tailwind utility soup into plain rgb() values.

Fonts and the type scale

The same collection already has them. Font families come back as full stacks, so take the first entry:

families = Counter(f.split(",")[0].strip('"\' ') for f in raw["fonts"])
print(families.most_common(5))

sizes = sorted(
    {float(s.replace("px", "")) for s, c in top(raw["sizes"], n=20)},
)
print(sizes)  # e.g. [12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 36.0, 56.0]

That sorted list is the site's type scale. If it has fifteen entries instead of seven, you are looking at a site without a design system, which is also useful to know.

From raw values to actual tokens

Raw counts are not tokens yet. Three cleanup steps matter:

  1. Drop the defaults. rgb(0, 0, 0) near the top does not mean the brand is black. Browser resets and inherited defaults pollute every element, so check whether a value actually appears on visible, styled elements before promoting it.
  2. Merge near-duplicates. rgb(99, 91, 255) and rgb(99, 92, 255) are the same token with a rounding disagreement. Snap channels to a small tolerance before counting, or you will split one token's count across five entries.
  3. Name by role, not by value. The most frequent saturated color on buttons and links is primary. The dominant text color is text. Frequency plus element context gets you there; k-means clustering is overkill for this and I would skip it.

Common problems

Real gotchas, in the order they will bite you:

  • Your viewport is a filter. Computed styles reflect the current viewport only. A @media (max-width: 640px) override is invisible at 1440px. Extract at two viewports (mobile and desktop) if you care about responsive tokens.
  • Cookie banners poison the palette. Consent overlays and chat widgets bring their own colors and fonts, and they are often thousands of DOM nodes. Dismiss them or exclude their containers before collecting.
  • Fonts can lie. font-family lists what CSS asked for, not what loaded. With font-display: swap the brand font may never arrive in a headless run. Await document.fonts.ready before collecting if exact loaded fonts matter.
  • Gradients are not in backgroundColor. They live in backgroundImage. If a brand is gradient-heavy, collect that property too or you will report their hero as plain white.
  • Shadow DOM and iframes are invisible to querySelectorAll. Web-component-heavy sites need you to walk shadowRoots explicitly.

DIY vs parsing CSS vs an API

ApproachTailwind / utility CSSRuntime CSS-in-JSResolves var() and cascadeMedia queriesMaintenance
Playwright + computed styles (this guide)YesYesYesCurrent viewport onlyBrowser infra is yours to run
Parse CSS files yourself (tinycss2, no browser)PainfulNoNoDeclared, but unused values includedLow, but wrong on modern sites
MiroMiro extract APIYesNo (static HTML + CSS, no JS execution)YesYes, all declared breakpointsNone

Use the Playwright approach if you are extracting from a handful of sites, need runtime-injected styles, and want full control. Skip the hand-rolled CSS-parsing route: resolving the cascade, specificity, and nested var() fallbacks yourself is a project, not a script.

Or use one API call

The dedupe, the ranking, the var() chains, the resets, and every declared breakpoint - that is what our extract endpoint does for a living:

curl "https://miromiro.app/api/v1/extract?url=stripe.com" \
  -H "Authorization: Bearer $MIROMIRO_API_KEY"

The JSON response has colors, fontFamilies, fontSizes, fontWeights, spacing, borderRadiuses, boxShadows, and more, already deduped and ranked. Trim it with ?fields=colors,spacing if you only need part of it.

If your script from this guide breaks on some site's cookie banner at 2am, that is roughly the moment an API earns its keep. You can get a free API key with 100 credits a month, no card, or try it without signing up in the playground.

Summary

Playwright plus getComputedStyle plus a Counter is the honest way to extract design tokens in Python, and the frequency counting matters more than any clever clustering. Parse-the-CSS approaches are not worth your time on modern sites. I hope this saved you a debugging evening.

Frequently asked questions

Can I extract design tokens without a headless browser?

You can parse the site's CSS with a library like tinycss2, but on modern sites this works badly: CSS-in-JS generates styles at runtime, Tailwind buries the palette in utility classes, and you cannot tell which declared values are actually used. Computed styles from a real browser are the ground truth.

How do I extract tokens from a Tailwind site where everything is utility classes?

Ignore the class names entirely and read computed styles. getComputedStyle resolves bg-indigo-600 to rgb(79, 70, 229) like any other CSS, so nothing in this guide changes.

Why do my extracted colors include values the site never visibly uses?

Three usual suspects: browser defaults on elements you collected, cookie banners and chat widgets injecting their own palette, and transparent or inherited values you forgot to filter.

How many pages should I sample?

One landing page is usually enough for brand color and typography. Add one product or app page for functional colors (success, error, info). More than three pages rarely changes the top tokens.

Skip the maintenance - one API call

Design tokens, brand data, fonts, SVGs, images, and section code from any URL. 100 free credits a month, no card.

S

Soraia · Founder, MiroMiro

Building MiroMiro: a browser extension and API that extract design tokens, assets, and production-ready code from any live website. The gotchas in these posts come from the extraction engine's own commit history.

Follow on X
design tokenspythonplaywrightcssweb extraction