design-system

SKILL.md

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

  1. Convert all collected colors to HSL for comparison
  2. Group similar colors (within ΔE < 5) to deduplicate near-identical values
  3. Rank by frequency of occurrence in the DOM
  4. Apply the detection rules above based on where each color appears
  5. 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.

Weekly Installs
2
First Seen
13 days ago
Installed on
opencode2
gemini-cli2
antigravity2
claude-code2
github-copilot2
codex2