extract-design

v2026.09.24

Extract a complete design system — colors, typography, spacing, components, shadows, and W3C design tokens — from any live website using Dembrandt. Runs a headless browser against the URL and returns real computed values from the DOM. Use when you need a site's actual design tokens, want to reverse-engineer a visual design, or need to seed a design system from an existing product.

GitHub
Install command
npx skhub add dembrandt/extract-design
Markdown
SKILL.md

Extract Design — Dembrandt

Dembrandt runs a headless Chromium browser against any URL, walks up to thousands of DOM elements, reads computed CSS, and returns a structured design system: colors with confidence scoring, typography styles, spacing scale, border radius, borders, shadows, and interactive component styles.

How to Run

# Zero-install — npx fetches the package on first run (lowest friction)
npx -y dembrandt https://dembrandt.com

# Or install once (global), then call `dembrandt` directly
npm i -g dembrandt

# Basic extraction — outputs to terminal
dembrandt https://dembrandt.com

# JSON output — pipe into files or other tools
dembrandt https://dembrandt.com --json-only > dembrandt-tokens.json

# W3C DTCG format (design-tokens.org standard)
dembrandt https://dembrandt.com --dtcg --save-output

# Generate DESIGN.md (human + AI readable brand doc)
dembrandt https://dembrandt.com --design-md

# Multi-page crawl (follows internal links)
dembrandt https://dembrandt.com --crawl 5

# Dark mode colors
dembrandt https://dembrandt.com --dark-mode

# Mobile viewport
dembrandt https://dembrandt.com --mobile

# Everything saved to output/
dembrandt https://dembrandt.com --save-output

# Tailwind v4 @theme CSS — observed values only  [dembrandt 0.28+]
dembrandt https://dembrandt.com --tailwind src/app.css

# Self-contained HTML report — open offline or attach as a CI artifact  [dembrandt 0.19+]
dembrandt https://dembrandt.com --html report.html

# Drift gate — compare against a saved baseline; exits 1 on drift  [dembrandt 0.19+]
dembrandt https://app.example.com --compare baseline.json --html report.html

MCP Usage (async by default)

To expose Dembrandt as MCP tools, add this server to the agent's MCP config (no install — npx fetches it on first run):

{ "mcpServers": { "dembrandt": { "command": "npx", "args": ["-y", "--package", "dembrandt", "dembrandt-mcp"] } } }

When using the Dembrandt MCP server, all extraction tools return a job_id immediately rather than blocking. Poll get_job_status until status is "completed":

1. get_design_tokens({ url: "dembrandt.com", pages: 5 })
   → { job_id: "job_123_abc", status: "queued" }

2. get_job_status({ job_id: "job_123_abc" })
   → { status: "running" }   // poll again

3. get_job_status({ job_id: "job_123_abc" })
   → { status: "completed", result: { ... } }

4. get_findings({ job_id: "job_123_abc" })      // no need to resend the extraction
   → { findings: [ ... ], contrast: { ... } }

Hand the job_id to the analysis tools instead of passing the extraction back. Every pure tool accepts it, and the queue keeps the whole extraction for an hour, so a job started by a narrow tool such as get_color_palette still feeds export_dtcg. Passing the extraction inline works and wins when you give both, but a real extraction is far too large to travel back through the model as a tool argument. [dembrandt 0.29+]

Pass sync: true to any extraction tool to block and return the result directly (useful on fast networks, risks timeout on slow sites, and proportionally slower when pages is above 1).

Extraction tools: get_design_tokens (everything), get_color_palette, get_typography, get_component_styles, get_surfaces, get_spacing, get_brand_identity, get_motion (durations, easings, named keyframes, hover patterns, and the gradients that travel with them) [dembrandt 0.36+]. All accept slow, mobile (mobile viewport), and cookie (cookie string for authenticated pages); get_design_tokens and get_color_palette also accept darkMode and wcag (contrast analysis). [dembrandt 0.23.1+ for mobile/cookie/wcag]

Every extraction tool also crawls, which is the single biggest lever on token quality: one page gives you one page's tokens. [dembrandt 0.29+]

OptionWhat
pagesExtract up to N pages and merge them into one token set (1 to 20). Pages come from DOM links, or from sitemap.xml when sitemap is true
pathsName the extra paths explicitly, e.g. ["/pricing", "/docs"]. Overrides discovery
sitemapDiscover from sitemap.xml. Alone it takes up to 20 pages; pages caps it
headerOne extra HTTP header, e.g. "Authorization: Bearer ...", for pages a cookie cannot reach
userAgentCustom user agent string
noSandboxDisable the browser sandbox. Required inside Docker and most CI containers, where launch otherwise fails

A page that fails to load is dropped and the merge carries the rest, so a crawl does not fail on one bad URL.

Pure tools (no browser, synchronous; the extraction goes in result, or name a completed job with job_id [dembrandt 0.29+]): compute_drift (0-100 drift score between two extractions; takes baselineJobId and candidateJobId as the job-based form), get_findings (design-system lint: contrast, consistency, duplication), export_dtcg (W3C Design Tokens format), generate_design_md (DESIGN.md brand guide), render_report (self-contained HTML report), export_tailwind (Tailwind v4 @theme block), export_shadcn (shadcn/ui theme, slots left at shadcn's defaults where the page supplied nothing). Job control: get_job_status, list_jobs, cancel_job. [dembrandt 0.23.1+ for get_findings/export_dtcg/generate_design_md/list_jobs, 0.36+ for the two emitters]

Three tools take their own input rather than an extraction [dembrandt 0.36+]: validate_dtcg (check a DTCG document against the 2025.10 spec, including one this server produced, so a hand edit cannot quietly break it), check_contrast (grade colour pairs you name against WCAG 2.1 at the threshold the text size earns, for colours you are about to ship rather than ones already on a page), and check_robots (ask whether robots.txt allows a URL before spending a browser run on it).

Note: npx runs a dembrandt-mcp already on PATH in preference to the version named in --package, so a globally installed dembrandt silently shadows the pinned one. Symptom: options the pinned version supports are rejected as unknown, or a crawl returns a single page. Check with dembrandt --version and upgrade the global install, or point the MCP config at an explicit path.

Note: dembrandt <=0.23.0 fails to start via the npx one-liner above (McpDepsMissingError) — the MCP SDK was an optional peer dependency. Fixed in 0.23.1; require it.

Output Structure

Dembrandt returns a structured object. The key sections:

colors.palette        — Deduplicated colors with confidence (high/medium/low).
                        Each entry carries hex (`normalized`), plus `lch` and
                        `oklch` of the same colour, and derived `role`,
                        `onColor`, `hover`. With `--wcag`, entries also carry
                        `contrastAgainst`, the pairs this colour was actually
                        observed against on the page, deduped by the other
                        colour and sorted by ratio (dembrandt 0.31+).
colors.detected       — Every colour that passed the alpha gate, with no
                        frequency threshold and no perceptual merge, so six
                        near-identical reds stay six entries. `usageFrac`
                        counts elements; `areaFrac` is the share of painted
                        background area, which ranks a hero fill above many
                        small glyphs the way a count alone does not (dembrandt
                        0.32+). Use `palette` for a design system and
                        `detected` when you need recall.
colors.semantic       — Primary, secondary, background, text, and accent detection
colors.cssVariables   — Named CSS custom properties. `value` is the author's
                        string verbatim (the only record of the authored
                        notation), plus computed hex + LCH + OKLCH.
typography.styles     — Font family, size, weight, line-height per context.
                        Each entry carries `count`, the number of elements
                        rendering that exact style. `code`, `pre`, `kbd` and
                        `samp` land in a `mono` context, so a mono face used
                        only in code blocks is visible (dembrandt 0.32+).
                        `isFluid` is read from the authored declaration, so a
                        `clamp()` or viewport-relative ramp is detected
                        (dembrandt 0.32+).
typography.sources    — Google Fonts, Adobe Fonts, variable font detection.
                        `urls` lists the resolved font asset and webfont
                        stylesheet URLs, deduped, so you can re-fetch or verify
                        the real files. `filteredFamilies` lists families
                        dropped by the usage floor — check it before concluding
                        a face is missing.
spacing.commonValues  — Margin/padding scale with rem equivalents
spacing.scaleType     — 4px, 8px, or custom grid. Since 0.34.0 the verdict is
                        weighted: a page reads custom unless 60% of its spacing
                        lands on the step, so sites that used to claim a grid
                        now report custom truthfully
borderRadius.values   — Border radius tokens with element context
borders.combinations  — Width + style + color combinations
shadows               — Box shadow elevation system. One string per shadow,
                        layers comma-separated as CSS writes them. The `--dtcg`
                        export splits them into structured layers (0.32+).
components.buttons    — Button variants with hover/active/focus states
components.inputs     — Input styles with focus states
components.links      — Link colors and hover states
components.badges     — Badge/tag/chip variants
breakpoints           — Responsive breakpoints from CSS media queries
frameworks            — Detected CSS framework (Tailwind, shadcn, MUI, etc.)
iconSystem            — Detected icon library (Heroicons, FA, Material, etc.)
pages                 — Present only on a merged multi-page result (`--crawl`,
                        `--sitemap`, extra paths, or MCP `pages`). One entry per
                        page extracted, so you can tell which URLs the merged
                        tokens came from. Palette entries then also carry
                        `pageCount`.
wcag                  : with `--wcag`, observed contrast pairs (fg, bg, ratio,
                        aa/aaLarge/aaa booleans) between real rendered colours.
meta.crawl            : present when `--crawl`, `--sitemap` or explicit paths
                        are used, with `technique`, `pagesRequested`,
                        `pagesFound` (dembrandt 0.31+).
meta.robotsWarnings   : pages robots.txt disallowed, whether that was the
                        entry URL or one discovered during a crawl. The check
                        is advisory by default, it does not block
                        extraction, and this is the only record of what it
                        flagged (dembrandt 0.31+). Set
                        `DEMBRANDT_ENFORCE_ROBOTS=1` to make a disallow, or a
                        robots.txt we could not read, skip the target with exit
                        `4`. A site with no robots.txt (404/410) is not a
                        refusal and still runs (dembrandt 0.32+).

Working with Extracted Tokens

Seeding a Tailwind theme (dembrandt 0.28+)

Don't hand-map the JSON. --tailwind writes a Tailwind v4 @theme block directly:

dembrandt https://dembrandt.com --tailwind            # → output/<domain>/theme.css
dembrandt https://dembrandt.com --tailwind src/app.css  # or straight into the project
@import "tailwindcss";

@theme {
  --color-primary: #ea580c;
  --text-display: 96px;
  --text-display--line-height: 1;
  --spacing: 8px;
  --radius-lg: 8px;
  --breakpoint-md: 700px;
}

Observed values only: no 50–950 shade ramps, no interpolated scale steps, no derived hover or on-colour variants. An invented shade is indistinguishable from a measured one once it is in the file, so the export is a starting point you extend by hand. Colours keep their semantic role name (--color-primary) or the page's own custom property name where one is declared; the rest are numbered --color-brand-N. Spacing collapses to v4's --spacing multiplier when the page has a base-N rhythm, and falls back to named steps otherwise. Tailwind's defaults still apply to anything not listed, so the block extends the theme rather than replacing it.

v4 only. For a v3 tailwind.config.js, map the output by hand — colors.semantic → theme.colors, typography.styles → fontFamily, spacing.commonValues → spacing, borderRadius.values → borderRadius, shadows → boxShadow.

Seeding a shadcn/ui theme

--shadcn (0.34.0+) writes the theme, so do not hand-map the variables:

dembrandt stripe.com --shadcn        # output/<domain>/shadcn.css

The file carries the :root block and the @theme inline mapping Tailwind v4 needs, in oklch. A slot is written only where the page supplied a value; the rest are named in the file header and left to shadcn's own defaults, so an unobserved slot never arrives as a plausible value that reads as measured. --dark-mode produces the .dark block, one colour scheme per run.

Do not derive --radius from borderRadius.values[0]: that list is sorted by length, so the first entry is the smallest radius on the page, not the one its buttons and inputs use. The flag takes the most-used value.

Reading confidence levels

Dembrandt scores every color by semantic context:

ConfidenceMeaning
highAppears on semantically labeled elements (buttons, CTAs, headers with brand classes). Almost certainly a brand color.
mediumModerate frequency or moderate context. Likely a brand color.
lowRare, low semantic context. May be a one-off or component-specific color.

Since 0.28.0 confidence also has a usage floor, as spacing and radii always had: a colour seen once caps at low, twice at medium, and high needs three occurrences whatever its semantic context scores. Hover and focus colours are the exception and keep medium — their single occurrence is provenance, not a usage claim.

Start with high confidence colors when building a palette. Include medium for full coverage. Treat low as reference only.

Colour notation

Never convert a colour by hand and never re-derive one with your own maths. Every palette entry and every CSS variable already carries lch and oklch alongside the hex, so read the field you need straight from the JSON. --color-format only changes what the terminal prints, so it is the wrong tool when you are consuming JSON or MCP output.

Use hex (normalized) as the identity of a colour: it is what dedup, drift comparison and every downstream tool key on. Two entries with the same hex are the same token even when their emitted notations differ. When an author declared a token in a modern notation, cssVariables[name].value preserves it exactly, which is what you want when writing CSS back into that codebase, since it keeps the author's own notation and stays inside their gamut.

Flags Reference

FlagWhat it does
--json-onlyClean JSON to stdout — pipe into files or tools
--save-outputSave JSON to output/<domain>/<timestamp>.json
--dtcgW3C Design Tokens Community Group format. A shadow token's $value is an array when the shadow has more than one layer, so a consumer reading $value.offsetX must branch on Array.isArray. (0.32+)
--design-mdGenerate DESIGN.md — prose-first brand doc
--html [path]Self-contained HTML report (inline CSS, embedded JSON). Open offline or attach as a CI artifact. (0.19+)
--compare <baseline.json>Diff against a saved extraction; prints a drift verdict and exits 1 on drift. CI gate. (0.19+)
--brand-guideGenerate a PDF brand guide
--dark-modeExtract dark color scheme and merge into palette
--mobileExtract at 390px mobile viewport
--crawl <n>Crawl up to N pages and merge tokens
--sitemapDiscover pages from sitemap.xml
--slow3× timeouts — use on slow-loading or JS-heavy sites
--screenshot <path>Save a full-page screenshot
--raw-colorsInclude pre-filter raw colors in JSON output
--color-format <fmt>Notation for colors printed to the terminal: hex (default), rgb, oklch, lch, source (as authored). Presentational only, so JSON output is unchanged, and export paths ignore it. (0.28+)
--tailwind [path]Write a Tailwind v4 @theme CSS file — observed values only. Defaults to output/<domain>/theme.css. (0.28+)
--shadcn [path]Write a shadcn/ui theme — observed slots only, @theme inline included. Defaults to output/<domain>/shadcn.css. (0.34+)
--browser firefoxUse Firefox instead of Chromium
--stealthOpt-in anti-detection: navigator spoofing + human mouse simulation. Use only when authorized.
--user-agent <string>Custom user agent string
--locale <string>Browser locale, e.g. fi-FI, en-GB (default: en-US)
--timezone <string>Browser timezone, e.g. Europe/Helsinki (default: America/New_York)
--accept-language <string>Custom Accept-Language header value
--screen-size <WxH>Physical screen resolution to report, e.g. 1920x1080

Drift Detection & CI (dembrandt 0.19+)

--compare turns extraction into a gate. Save a known-good baseline, then compare later extractions against it:

# 1. capture a baseline (in the SAME environment you will check against)
dembrandt https://app.example.com --json-only > baseline.json

# 2. later — compare; exits 0 if stable, 1 if drifted
dembrandt https://app.example.com --compare baseline.json --html report.html
  • Runs the canonical drift engine over structured tokens — deterministic, not a pixel/render diff.
  • Exit code: 0 stable, 1 drift. Gates a pipeline directly.
  • --html writes a self-contained report; with --compare it includes a drift banner (added/removed/changed tokens). Attach it as a CI artifact.

Baselines churn once on 0.28.0. Three fixes move colour and typography values: the palette usage floor, body ending at the 24px reading range (non-heading text above it takes text, so hero copy stops landing on the body token), and families under 2% of counted text being dropped. Measured on dembrandt.com against a 0.27.1 extraction, drift came out at 15 against a threshold of 10 — enough to fail a gate. On the first run after upgrading, re-approve with --compare <baseline> --approve or regenerate the baseline. Drift after that is real drift.

0.32.0 needs no re-approval. Schema 1.12.0 measured 7 and 6 against a threshold of 10 on two reference sites, the only difference being the added mono context. The Tailwind shadow ladder does reorder by depth rather than blur alone, so --shadow-sm/md/lg/xl can move for an unchanged site.

0.35.0 changes what the gate can fail on. Before it, a changed brand colour was divided by every palette entry that stayed the same: a real stripe.com baseline with semantic.primary turned magenta scored stable and exited 0. The semantic map is scored on its own weight now, so a moved role reaches the threshold. Tolerance for run-to-run variance is unchanged, and a single palette entry appearing or vanishing still does not gate. Measured on two reference sites against 0.34.2: 0 and 4 against a threshold of 10, so no re-approval is needed for a site that did not change. A site whose brand colour genuinely moved will fail a gate that passed before.

Also on 0.35.0: off-grid spacing findings were never produced at all, because the check compared against a spacing.scaleType spelling the extractor stopped writing in 1.14.0. Sites with off-grid values now carry the finding and a lower consistency score.

Determinism: capture the baseline in the same environment you check it in (both production, or both the same preview). A baseline from one environment compared against another shows false drift.

In CI: run --compare <baseline> --html report.html against a preview/deployed URL, fail the job on exit 1, upload the HTML artifact. Programmatic: import computeDrift from dembrandt/drift and generateHtmlReport from dembrandt/report to diff and render server-side without the CLI.

Anti-Bot and SPA Handling

Dembrandt handles common extraction challenges automatically:

  • SPA hydration — waits 8s for React/Vue/Svelte to render before extracting
  • Lazy content — scrolls the full page to trigger lazy-loaded components
  • Cloudflare / bot walls — auto-retries with a visible browser if headless is blocked
  • Slow sites — use --slow for 3× timeouts on heavy JS bundles
  • Cookie banners — dismisses common CMP dialogs (OneTrust, cookielaw, GDPR patterns) automatically
  • Bot detection bypass — use --stealth to opt in to navigator spoofing and human mouse simulation; off by default so the tool identifies itself honestly
  • robots.txt — read once per origin and matched against the User-Agent the browser actually sends. Advisory by default; set DEMBRANDT_ENFORCE_ROBOTS=1 for scheduled jobs and server-side use, where nobody is deciding what may be fetched, and a disallow or a file we could not read skips the target with exit 4. A missing robots.txt is not a refusal (0.32+)

Checklist After Extraction

  • Identify the 3–5 high-confidence colors — these are the core brand palette
  • Check colors.semantic.primary — is it correct?
  • Look at typography.styles — what are the heading and body fonts?
  • Check spacing.scaleType — 4px, 8px, or custom? custom is a real answer, not a gap
  • Review components.buttons — how many variants exist?
  • Check frameworks — is Tailwind, shadcn, or MUI detected? This shapes how you apply the tokens.
  • Use --dark-mode if the site has a dark theme
  • Use --crawl 3 if the site has a multi-section design system spread across routes
Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

MIT

Source path

skills/extract-design

Default branch

main

Latest commit

05a50eb

Tree SHA

ff76672