design-system
This skill extracts a complete design system from a live website using browser automation. It navigates the rendered DOM, captures every visual design decision, and outputs ready-to-use design tokens that plug directly into the frontend-design skill.
The user provides: the URL of the website to extract from, and optionally the output directory (default: design-system/).
Why Extract a Design System
Manually inspecting DevTools to extract colors, fonts, spacing, and other design tokens is slow and error-prone. When rebuilding a site or building a demo that must match a client's existing brand, you need every visual decision captured accurately:
- Colors — not just the primary brand color, but every neutral, accent, and semantic color in use
- Typography — exact font families, sizes for each heading level, weights, line heights
- Spacing — the rhythm of padding and gaps that makes a design feel cohesive
- Components — button styles, card radii, shadow depths that define the UI language
This skill automates the full extraction in minutes instead of hours.
What Gets Extracted
| Category | Details |
|---|---|
| Logo | SVG/PNG from header, footer, favicon — downloaded to assets/ |
| Colors | CSS custom properties + computed background/text/border/fill colors, classified into primary/secondary/accent/neutral/semantic roles |
| Typography | Font families (+ Google Fonts/font-face source URLs), sizes for h1-h6/body/small, weights, line heights, letter spacing |
| Border radius | All unique values from buttons, cards, inputs, containers |
| Shadows | box-shadow values from cards, dropdowns, modals, buttons |
| Spacing | Common padding, gap, and margin patterns |
| Buttons | Full button style specs — colors, radius, padding, font, hover states, variants |
| Icons | Library detection (Font Awesome, Material, Heroicons, Lucide, Phosphor, etc.) |
| Layout | Max-width, container padding, grid/flex usage, breakpoints |
| Dark mode | Detect and extract dark theme overrides if present |
Extraction Process
Step 1: Navigate and Prepare
1. Navigate to the target URL with agent-browser (fallback: Playwright MCP browser_navigate)
2. Wait for full page load (networkidle)
3. Dismiss cookie consent banners — look for "Reject all", "Accept", or close buttons
4. Scroll the full page to trigger lazy-loaded content and fonts
5. Wait 2 seconds for web fonts to finish loading
Step 2: Extract CSS Custom Properties
Extract all CSS custom properties from :root and theme stylesheets:
// Execute via agent-browser evaluate or Playwright browser_evaluate
(() => {
const vars = {};
// From inline styles on :root
const root = document.documentElement;
const rootStyles = getComputedStyle(root);
// From stylesheets
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule.selectorText === ':root' || rule.selectorText === ':root, :host') {
for (const prop of rule.style) {
if (prop.startsWith('--')) {
vars[prop] = rule.style.getPropertyValue(prop).trim();
}
}
}
}
} catch (e) { /* CORS stylesheet — skip */ }
}
return vars;
})()
This captures design tokens the site already defines (Tailwind, CSS-in-JS theme vars, custom property systems).
Step 3: Extract Colors from Computed Styles
Scan the rendered DOM to find every color actually in use:
(() => {
const colors = { background: new Set(), text: new Set(), border: new Set(), fill: new Set() };
const elements = document.querySelectorAll('*');
const sample = Array.from(elements).slice(0, 2000); // Cap for performance
for (const el of sample) {
const s = getComputedStyle(el);
const bg = s.backgroundColor;
const tc = s.color;
const bc = s.borderColor;
if (bg && bg !== 'rgba(0, 0, 0, 0)' && bg !== 'transparent') colors.background.add(bg);
if (tc) colors.text.add(tc);
if (bc && bc !== 'rgba(0, 0, 0, 0)' && bc !== bg) colors.border.add(bc);
// SVG fills
if (el.tagName === 'svg' || el.closest('svg')) {
const fill = s.fill;
if (fill && fill !== 'none' && fill !== 'rgba(0, 0, 0, 0)') colors.fill.add(fill);
}
}
return {
background: [...colors.background],
text: [...colors.text],
border: [...colors.border],
fill: [...colors.fill]
};
})()
Step 4: Extract Typography
Capture all font families, sizes, weights, and line heights:
(() => {
const typography = {
families: new Set(),
headings: {},
body: null,
small: null
};
// Heading styles
for (let i = 1; i <= 6; i++) {
const el = document.querySelector(`h${i}`);
if (el) {
const s = getComputedStyle(el);
typography.headings[`h${i}`] = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing,
textTransform: s.textTransform
};
typography.families.add(s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''));
}
}
// Body text
const body = document.querySelector('p');
if (body) {
const s = getComputedStyle(body);
typography.body = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight,
letterSpacing: s.letterSpacing
};
typography.families.add(s.fontFamily.split(',')[0].trim().replace(/['"]/g, ''));
}
// Small text
const small = document.querySelector('small, .text-sm, .text-xs, figcaption, .caption');
if (small) {
const s = getComputedStyle(small);
typography.small = {
fontFamily: s.fontFamily,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
lineHeight: s.lineHeight
};
}
// Detect font sources
const fontLinks = [];
document.querySelectorAll('link[href*="fonts.googleapis.com"], link[href*="fonts.bunny.net"]').forEach(link => {
fontLinks.push(link.href);
});
// Check @font-face rules
const fontFaces = [];
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSFontFaceRule) {
fontFaces.push({
family: rule.style.getPropertyValue('font-family').replace(/['"]/g, ''),
src: rule.style.getPropertyValue('src'),
weight: rule.style.getPropertyValue('font-weight'),
style: rule.style.getPropertyValue('font-style')
});
}
}
} catch (e) { /* CORS */ }
}
return {
families: [...typography.families],
headings: typography.headings,
body: typography.body,
small: typography.small,
fontSources: fontLinks,
fontFaces
};
})()
Step 5: Extract Border Radius, Spacing, and Shadows
(() => {
const radii = new Set();
const shadows = new Set();
const paddings = new Set();
const gaps = new Set();
const interactive = document.querySelectorAll(
'button, a, input, select, textarea, [class*="card"], [class*="Card"], ' +
'[class*="btn"], [class*="Btn"], [class*="modal"], [class*="Modal"], ' +
'[class*="dropdown"], [class*="Dropdown"], [role="dialog"], [role="button"]'
);
for (const el of interactive) {
const s = getComputedStyle(el);
const r = s.borderRadius;
if (r && r !== '0px') radii.add(r);
const shadow = s.boxShadow;
if (shadow && shadow !== 'none') shadows.add(shadow);
}
// Spacing from containers and sections
const containers = document.querySelectorAll(
'section, main, article, [class*="container"], [class*="wrapper"], ' +
'[class*="Container"], [class*="Wrapper"], header, footer, nav'
);
for (const el of containers) {
const s = getComputedStyle(el);
const p = s.padding;
if (p && p !== '0px') paddings.add(p);
const g = s.gap;
if (g && g !== 'normal' && g !== '0px') gaps.add(g);
}
return {
borderRadius: [...radii].sort(),
boxShadows: [...shadows],
paddings: [...paddings],
gaps: [...gaps]
};
})()
Step 6: Extract Button Styles and Component Patterns
(() => {
const buttons = [];
const btnElements = document.querySelectorAll(
'button, a[class*="btn"], a[class*="Btn"], a[class*="button"], a[class*="Button"], ' +
'[role="button"], input[type="submit"]'
);
const seen = new Set();
for (const el of btnElements) {
const s = getComputedStyle(el);
const key = `${s.backgroundColor}|${s.color}|${s.borderRadius}|${s.fontSize}|${s.fontWeight}|${s.padding}`;
if (seen.has(key)) continue;
seen.add(key);
buttons.push({
text: el.textContent?.trim().substring(0, 50),
backgroundColor: s.backgroundColor,
color: s.color,
borderRadius: s.borderRadius,
border: s.border,
padding: s.padding,
fontSize: s.fontSize,
fontWeight: s.fontWeight,
fontFamily: s.fontFamily,
textTransform: s.textTransform,
letterSpacing: s.letterSpacing,
boxShadow: s.boxShadow,
cursor: s.cursor
});
}
return buttons;
})()
Step 7: Extract Logos and Detect Icon Libraries
(() => {
const logos = [];
// Header/footer logos
const logoSelectors = [
'header img[class*="logo"], header img[alt*="logo"], header svg[class*="logo"]',
'footer img[class*="logo"], footer img[alt*="logo"], footer svg[class*="logo"]',
'a[class*="logo"] img, a[class*="Logo"] img',
'a[class*="logo"] svg, a[class*="Logo"] svg',
'[class*="brand"] img, [class*="Brand"] img',
'header a:first-child img, header a:first-child svg'
];
for (const selector of logoSelectors) {
document.querySelectorAll(selector).forEach(el => {
if (el.tagName === 'IMG') {
logos.push({ type: 'img', src: el.src, alt: el.alt, width: el.naturalWidth, height: el.naturalHeight });
} else if (el.tagName === 'svg' || el.tagName === 'SVG') {
logos.push({ type: 'svg', html: el.outerHTML.substring(0, 5000) });
}
});
}
// Favicon
const favicon = document.querySelector('link[rel="icon"], link[rel="shortcut icon"], link[rel="apple-touch-icon"]');
if (favicon) logos.push({ type: 'favicon', href: favicon.href });
// Icon library detection
const iconLibraries = [];
const checks = [
{ name: 'Font Awesome', test: () => !!document.querySelector('[class*="fa-"], [class*="fas "], [class*="far "], [class*="fab "]') },
{ name: 'Material Icons', test: () => !!document.querySelector('.material-icons, .material-symbols-outlined, [class*="mdi-"]') },
{ name: 'Heroicons', test: () => !!document.querySelector('[class*="heroicon"], [data-slot="icon"]') },
{ name: 'Lucide', test: () => !!document.querySelector('[class*="lucide-"], .lucide') },
{ name: 'Phosphor', test: () => !!document.querySelector('[class*="ph-"]') },
{ name: 'Bootstrap Icons', test: () => !!document.querySelector('[class*="bi-"]') },
{ name: 'Feather', test: () => !!document.querySelector('[class*="feather-"]') },
{ name: 'Tabler Icons', test: () => !!document.querySelector('[class*="tabler-"], [class*="ti-"]') },
{ name: 'Inline SVG icons', test: () => document.querySelectorAll('svg[viewBox]').length > 5 }
];
for (const check of checks) {
try { if (check.test()) iconLibraries.push(check.name); } catch (e) {}
}
return { logos, iconLibraries };
})()
Step 8: Extract Layout Tokens
(() => {
const layout = {};
// Max-width of main container
const containers = document.querySelectorAll(
'main, [class*="container"], [class*="Container"], [class*="wrapper"], [class*="Wrapper"]'
);
const maxWidths = new Set();
for (const el of containers) {
const s = getComputedStyle(el);
if (s.maxWidth && s.maxWidth !== 'none') maxWidths.add(s.maxWidth);
}
layout.maxWidths = [...maxWidths];
// Container padding
const mainEl = document.querySelector('main') || document.querySelector('[class*="container"]');
if (mainEl) {
const s = getComputedStyle(mainEl);
layout.containerPadding = { left: s.paddingLeft, right: s.paddingRight };
}
// Grid usage
const gridElements = [];
document.querySelectorAll('*').forEach(el => {
const s = getComputedStyle(el);
if (s.display === 'grid' || s.display === 'inline-grid') {
gridElements.push({
tag: el.tagName.toLowerCase(),
className: el.className?.toString().substring(0, 100),
gridTemplateColumns: s.gridTemplateColumns,
gap: s.gap
});
}
});
layout.grids = gridElements.slice(0, 10);
// Breakpoints from media queries
const breakpoints = new Set();
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSMediaRule) {
const match = rule.conditionText?.match(/(\d+)px/g);
if (match) match.forEach(bp => breakpoints.add(bp));
}
}
} catch (e) { /* CORS */ }
}
layout.breakpoints = [...breakpoints].sort((a, b) => parseInt(a) - parseInt(b));
return layout;
})()
Step 9: Detect Dark Mode
(() => {
const darkMode = { detected: false, method: null, tokens: {} };
// Check for prefers-color-scheme media queries
for (const sheet of document.styleSheets) {
try {
for (const rule of sheet.cssRules) {
if (rule instanceof CSSMediaRule && rule.conditionText?.includes('prefers-color-scheme: dark')) {
darkMode.detected = true;
darkMode.method = 'prefers-color-scheme';
// Extract dark mode custom properties
for (const innerRule of rule.cssRules) {
if (innerRule.selectorText === ':root' || innerRule.selectorText?.includes(':root')) {
for (const prop of innerRule.style) {
if (prop.startsWith('--')) {
darkMode.tokens[prop] = innerRule.style.getPropertyValue(prop).trim();
}
}
}
}
}
}
} catch (e) { /* CORS */ }
}
// Check for .dark or [data-theme="dark"] class-based toggle
if (!darkMode.detected) {
const hasDarkClass = !!document.querySelector('[class*="dark:"], .dark');
const hasDarkData = !!document.querySelector('[data-theme="dark"], [data-mode="dark"]');
if (hasDarkClass || hasDarkData) {
darkMode.detected = true;
darkMode.method = 'class-based';
}
}
// Check for a dark mode toggle button
const toggleSelectors = [
'[class*="theme-toggle"], [class*="dark-mode"], [class*="darkMode"]',
'[aria-label*="dark"], [aria-label*="theme"], [aria-label*="mode"]',
'button[class*="moon"], button[class*="sun"]'
];
for (const sel of toggleSelectors) {
if (document.querySelector(sel)) {
darkMode.toggleFound = true;
break;
}
}
return darkMode;
})()
Step 10: Take Visual Reference Screenshots
Capture key sections for visual reference:
1. Full-page screenshot → screenshots/full-page.png
2. Navigation/header at desktop width → screenshots/navigation.png
3. Hero section (first screen) → screenshots/hero.png
4. A card or grid component (if present) → screenshots/cards.png
5. Buttons (find a section with CTAs) → screenshots/buttons.png
6. Footer → screenshots/footer.png
Use agent-browser screenshot or Playwright browser_take_screenshot with element selectors.
Step 11: Validate on Inner Pages
Visit 2-3 inner pages (e.g., an about page, a detail page, a blog post) and re-run Steps 3-6 to confirm design consistency. Note any deviations — some sites use different color schemes or typography on certain page types.
For each inner page:
1. Navigate to the page
2. Re-extract colors, typography, and button styles
3. Compare with homepage extraction
4. Note differences in the output (e.g., "Blog pages use a narrower max-width")
Color Classification Strategy
Raw computed colors need to be classified into semantic roles for the design tokens. Use this strategy:
Automatic Classification Rules
| Role | Detection method |
|---|---|
| Background | Most common background-color on body, main, large sections |
| Foreground | Most common color on body, p, span (the main text color) |
| Primary | Color used on primary buttons, main CTAs, active nav items, links |
| Secondary | Second most prominent brand color — secondary buttons, badges, accents |
| Accent | Highlight color for hover states, focus rings, decorative elements |
| Muted | Low-contrast text (captions, placeholders, disabled states), subtle borders |
| Muted foreground | Text color used on muted backgrounds |
| Card | Background color of card components (often slightly off-white or a tinted neutral) |
| Border | Most common border-color on inputs, cards, dividers |
| Destructive | Red/error color — form errors, delete buttons, warning alerts |
| Success | Green — success messages, checkmarks, positive indicators |
| Warning | Yellow/amber — warning alerts, pending states |
Classification Process
- Convert all collected colors to HSL for comparison
- Group similar colors (within ΔE < 5) to deduplicate near-identical values
- Rank by frequency of occurrence in the DOM
- Apply the detection rules above based on where each color appears
- If a role can't be auto-detected, leave it blank and note it in the output
Output File Specifications
The skill produces these files in the output directory:
design-system/
├── design-tokens.md # Human-readable design system documentation
├── tokens.css # CSS custom properties ready for globals.css
├── tailwind-theme.ts # Tailwind v4 @theme block + v3 config fallback
├── screenshots/ # Visual references
│ ├── full-page.png
│ ├── navigation.png
│ ├── hero.png
│ ├── cards.png
│ ├── buttons.png
│ └── footer.png
└── assets/ # Downloaded logos and favicons
├── logo.svg (or logo.png)
├── logo-footer.svg
└── favicon.ico
design-tokens.md
# Design System — {domain}
**Extracted**: {date}
**Source URL**: {url}
**Pages analyzed**: {count}
## Colors
### Brand
| Role | Value | Preview |
|------|-------|---------|
| Primary | #8e375c | 🟣 |
| Secondary | #2d5a7b | 🔵 |
| Accent | #d4a853 | 🟡 |
### Neutrals
| Role | Value | Preview |
|------|-------|---------|
| Background | #f8f7f5 | ⬜ |
| Foreground | #1c1917 | ⬛ |
| Muted | #a8a29e | 🔘 |
| Border | #e7e5e4 | ▫️ |
### Semantic
| Role | Value |
|------|-------|
| Destructive | #dc2626 |
| Success | #16a34a |
| Warning | #d97706 |
### Dark Mode
| Role | Light | Dark |
|------|-------|------|
| Background | #f8f7f5 | #1c1917 |
| Foreground | #1c1917 | #f5f5f4 |
| Primary | #8e375c | #c76b94 |
| Muted | #a8a29e | #78716c |
## Typography
### Font Families
| Role | Family | Source |
|------|--------|--------|
| Headings | Playfair Display | Google Fonts |
| Body | DM Sans | Google Fonts |
### Scale
| Element | Size | Weight | Line Height | Letter Spacing |
|---------|------|--------|-------------|----------------|
| h1 | 48px | 700 | 1.2 | -0.02em |
| h2 | 36px | 700 | 1.25 | -0.01em |
| h3 | 24px | 600 | 1.3 | 0 |
| h4 | 20px | 600 | 1.4 | 0 |
| h5 | 18px | 600 | 1.4 | 0 |
| h6 | 16px | 600 | 1.5 | 0 |
| Body | 16px | 400 | 1.6 | 0 |
| Small | 14px | 400 | 1.5 | 0 |
## Border Radius
| Usage | Value |
|-------|-------|
| Buttons | 8px |
| Cards | 12px |
| Inputs | 6px |
| Full round | 9999px |
## Shadows
| Usage | Value |
|-------|-------|
| Card | 0 1px 3px rgba(0,0,0,0.1) |
| Dropdown | 0 4px 12px rgba(0,0,0,0.15) |
| Modal | 0 8px 30px rgba(0,0,0,0.2) |
## Spacing
| Pattern | Value |
|---------|-------|
| Section padding | 80px (py-20) |
| Card padding | 24px |
| Container max-width | 1280px |
| Container padding | 16px mobile / 32px desktop |
| Grid gap | 24px |
## Buttons
| Variant | BG | Text | Radius | Padding | Font |
|---------|-----|------|--------|---------|------|
| Primary | #8e375c | #ffffff | 8px | 12px 24px | DM Sans 500 |
| Secondary | transparent | #8e375c | 8px | 12px 24px | DM Sans 500 |
| Ghost | transparent | #1c1917 | 8px | 12px 24px | DM Sans 500 |
## Icons
- Library: Lucide React
- Style: Outline, 24px default
## Layout
- Max width: 1280px
- Container padding: 16px (mobile) / 32px (desktop)
- Grid: CSS Grid, 3-column at desktop
- Breakpoints: 640px, 768px, 1024px, 1280px
tokens.css
The CSS variable names match the frontend-design skill's @theme convention so the output can be pasted directly into globals.css:
/* Design System — {domain}
* Extracted: {date}
* Source: {url}
*/
@import "tailwindcss";
@theme {
/* Colors */
--color-background: {background};
--color-foreground: {foreground};
--color-primary: {primary};
--color-primary-foreground: {primary-foreground};
--color-secondary: {secondary};
--color-secondary-foreground: {secondary-foreground};
--color-accent: {accent};
--color-accent-foreground: {accent-foreground};
--color-muted: {muted};
--color-muted-foreground: {muted-foreground};
--color-card: {card};
--color-card-foreground: {card-foreground};
--color-border: {border};
--color-destructive: {destructive};
/* Typography */
--font-heading: "{heading-font}", serif;
--font-body: "{body-font}", sans-serif;
/* Border Radius */
--radius-sm: {radius-sm};
--radius-md: {radius-md};
--radius-lg: {radius-lg};
--radius-full: 9999px;
/* Shadows */
--shadow-sm: {shadow-sm};
--shadow-md: {shadow-md};
--shadow-lg: {shadow-lg};
}
/* Dark mode overrides */
@media (prefers-color-scheme: dark) {
:root {
--color-background: {dark-background};
--color-foreground: {dark-foreground};
--color-primary: {dark-primary};
--color-muted: {dark-muted};
--color-muted-foreground: {dark-muted-foreground};
--color-card: {dark-card};
--color-border: {dark-border};
}
}
tailwind-theme.ts
/* Design System — {domain}
* Extracted: {date}
*
* Tailwind v4: Copy the @theme block from tokens.css into globals.css.
* Tailwind v3: Use this config object in tailwind.config.ts.
*/
// Tailwind v3 fallback config
import type { Config } from "tailwindcss";
export default {
theme: {
extend: {
colors: {
background: "{background}",
foreground: "{foreground}",
primary: {
DEFAULT: "{primary}",
foreground: "{primary-foreground}",
},
secondary: {
DEFAULT: "{secondary}",
foreground: "{secondary-foreground}",
},
accent: {
DEFAULT: "{accent}",
foreground: "{accent-foreground}",
},
muted: {
DEFAULT: "{muted}",
foreground: "{muted-foreground}",
},
card: {
DEFAULT: "{card}",
foreground: "{card-foreground}",
},
border: "{border}",
destructive: "{destructive}",
},
fontFamily: {
heading: ["{heading-font}", "serif"],
body: ["{body-font}", "sans-serif"],
},
borderRadius: {
sm: "{radius-sm}",
md: "{radius-md}",
lg: "{radius-lg}",
},
boxShadow: {
sm: "{shadow-sm}",
md: "{shadow-md}",
lg: "{shadow-lg}",
},
},
},
} satisfies Partial<Config>;
Agent Team Integration
This skill runs in Phase 1 of website-refactor, in parallel with website-analysis:
Phase 1: Extract + Plan
├── website-analysis → structure + content (pages, text, images)
├── design-system → visual tokens (colors, fonts, spacing, components)
└── Lead plans spec, asks user questions
Phase 2: Project Setup (Lead scaffolds, applies design tokens to globals.css)
Phase 3: Design (designer uses extracted tokens from design-system/)
Phase 4+5: Verify + Audit
The designer teammate receives the extracted tokens:
"Design tokens are in design-system/tokens.css — copy the @theme block
into globals.css. Reference design-system/design-tokens.md for the full
type scale, spacing, and button specs. Visual references are in
design-system/screenshots/."
Standalone Usage
- Client demos: "Extract their brand's design system so our rebuild matches exactly"
- Competitor analysis: "What design tokens does competitor.com use?"
- Brand audits: "Document the current design system before we propose changes"
- Design handoff: "Extract the live site's design system for the design team"
- Migration prep: "Capture all visual decisions before rebuilding on a new stack"
Troubleshooting
Cookie Consent Banners
Always dismiss cookie banners first — they overlay the page and can block element inspection. Look for "Reject all", "Accept all", or close (×) buttons. Some banners use iframes; check inside iframes if the main document has no banner buttons.
CORS-Restricted Stylesheets
External stylesheets (Google Fonts, CDN-hosted CSS) may throw CORS errors when reading cssRules. The extraction snippets catch these errors and skip those sheets. Font information is still captured from computed styles and <link> elements.
Lazy-Loaded Fonts
Some sites load fonts only when specific sections scroll into view. Scroll the full page before extracting typography to ensure all fonts are loaded. Check document.fonts.status === 'loaded' or wait for document.fonts.ready.
Single-Page Applications (SPAs)
SPAs render content client-side. Always use agent-browser (real browser) — never curl or fetch(). Wait for networkidle after navigation. For React/Vue/Angular sites, content may render after initial load; add a short delay.
CSS-in-JS (Styled Components, Emotion)
Sites using CSS-in-JS generate styles at runtime. The computed style extraction (Steps 3-6) captures these correctly since it reads getComputedStyle() from the rendered DOM, not from stylesheets. CSS custom properties defined in JS theme objects will also appear in the :root extraction.
Tailwind CSS Sites
Tailwind sites often have no CSS custom properties — all styles are utility classes. The computed style extraction still works because it reads final rendered values. Look for a tailwind.config or @theme block in the page source for additional token definitions.
Dark Mode Toggle
If the site has a class-based dark mode toggle (.dark class), click the toggle button and re-run the color extraction to capture dark mode values. Report both light and dark tokens. If using prefers-color-scheme, the media query extraction (Step 9) captures it automatically.
Sites Behind Authentication
If the site requires login, note it and ask the user to provide credentials or a session cookie. Some extractions can still be done on public pages (login page, marketing pages).
Very Large Sites
The DOM sampling in Step 3 caps at 2000 elements for performance. For very large pages, this captures the most important elements (they appear first in DOM order). Run the extraction on the homepage and 2-3 key inner pages rather than every page.
More from saccoai/agent-skills
website-analysis
Crawl any website in a single pass to produce both a complete structural map and full content extraction. Discovers all pages, routes, navigation, multilingual variants, and issues while simultaneously extracting all text, images, metadata, and assets. Use before any migration, redesign, or audit.
16nextjs-fullstack
Opinionated Next.js fullstack patterns — App Router, Tailwind CSS v4, shadcn/ui, Better Auth, Drizzle ORM, Server Actions, and Vercel deployment. Use when scaffolding a new project or enforcing consistent architecture across client projects.
13seo-migration
SEO preservation during website migrations — redirect mapping, canonical URLs, sitemap generation, structured data, meta tags, and Search Console verification. Use when rebuilding a site to ensure zero SEO loss from URL changes, content moves, or domain switches.
9project-handoff
Generate complete client handoff documentation — deployment guide, environment setup, CMS instructions, maintenance checklist, architecture overview, and operational runbook. Use when delivering a finished project to a client or their team.
8client-proposal
Generate a professional project proposal from a website audit. Analyzes the prospect's current site, identifies issues, and produces a structured proposal with scope, deliverables, tech recommendations, and phased timeline. Use as a sales tool or for scoping client engagements.
6web-audit
Comprehensive website quality audit — Lighthouse scores, accessibility (axe-core), cross-browser testing, performance budgets, and mobile responsiveness. Generates actionable reports with pass/fail per page. Use to audit any live website or as a QA gate before deployment.
6