All skills
jakubkrehel avatar

/explain-interface

@267330e
by Jakub Kreheljakubkrehel/skills7.4k stars
275

Helps you figure out how something was built on the web.

Use this Skill: https://skilld.dev/gh/jakubkrehel/skills/explain-interface

This session only. Nothing lands on disk.

read-the-system.md

≈2k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Reading the whole system

Use this where no specific effect was named and the question is how the interface is built in general. For one named thing, use find-the-effect.md.

Every snippet runs in the page context, through whatever browser tooling is available: evaluate_script in the Chrome DevTools MCP, the console, a Playwright page.evaluate. Each returns data rather than printing, so the result comes back whole.

Run them in this order. Tokens first, because a page that hands you its custom properties has already told you most of the answer.

The gotcha that costs you the run

sheet.cssRules throws on a cross-origin stylesheet. Every snippet that walks stylesheets wraps the access and reports what it could not read. An explanation that silently skipped the main stylesheet describes a page nobody is looking at.

The stack first

"How was this site built" wants the frontend named before a type scale. Run this, then report each hit with its evidence, never as a bare claim:

const html = document.documentElement;
const res = performance.getEntriesByType('resource').map(r => r.name);
const any = re => res.some(n => re.test(n));
const attr = sel => !!document.querySelector(sel);
({
  framework: {
    next: !!window.__NEXT_DATA__ || any(/\/_next\/static/),
    nextAppRouter: typeof self.__next_f !== 'undefined',
    nuxt: !!window.__NUXT__ || any(/\/_nuxt\//),
    remix: !!window.__remixContext,
    gatsby: !!window.___gatsby,
    astro: attr('astro-island, [data-astro-cid]'),
    svelte: attr('[class*="svelte-"]') || any(/\/_app\/immutable\//),
    angular: attr('[ng-version]'),
    reactFiber: Object.keys(document.body.firstElementChild ?? {}).some(k => k.startsWith('__react')),
  },
  styling: {
    tailwind: getComputedStyle(html).getPropertyValue('--tw-ring-offset-width') !== ''
              || !!document.querySelector('[class*="bg-linear-to"], [class*="bg-gradient-to"]'),
    tailwindV4: !!document.querySelector('[class*="bg-linear-to"]'),
    cssModules: attr('[class*="_"][class*="__"]'),
    styledComponents: attr('[class^="sc-"]') || attr('style[data-styled]'),
    emotion: attr('[class^="css-"]'),
  },
  components: {
    radix: attr('[data-radix-popper-content-wrapper], [data-radix-scroll-area-viewport]')
           || !!document.querySelector('[data-slot], [data-state][data-side]'),
    baseUi: attr('[data-base-ui-portal], [class*="base-ui"]'),
    headlessUi: attr('[data-headlessui-state]'),
    mui: attr('[class*="Mui"]'),
    arkOrChakra: attr('[data-scope][data-part]'),
  },
  motion: { animationsRunning: document.getAnimations().length, gsap: !!window.gsap },
  images: { nextImage: any(/\/_next\/image\?/), modernFormats: [...document.images].some(i => /\.(avif|webp)/.test(i.currentSrc)), srcset: [...document.images].filter(i => i.srcset).length },
  fonts: { count: document.fonts.size, variable: [...document.fonts].some(f => String(f.weight).includes(' ')), selfHosted: !any(/fonts\.g(oogleapis|static)\.com/) },
});

Two rules. A fingerprint is not a fact, so give the evidence: /_next/static in an asset path is strong, a utility-looking class name alone is weak. And a false is not an absence, only a fingerprint that did not fire.

Tokens

const tokens = {}; const unreadable = [];
for (const sheet of document.styleSheets) {
  let rules; try { rules = sheet.cssRules } catch { unreadable.push(sheet.href); continue }
  for (const r of rules ?? []) {
    if (r.selectorText === ':root' || r.selectorText === 'html') {
      for (const prop of r.style) {
        if (prop.startsWith('--')) tokens[prop] = r.style.getPropertyValue(prop).trim();
      }
    }
  }
}
({ tokens, unreadable, count: Object.keys(tokens).length });

Group the result by prefix. The prefixes are the system's own layer names, and a two-tier structure, --blue-500 feeding --color-text-primary, is the seam better-colors calls the semantic tier.

The type scale

Leaf text nodes only, so a wrapper's inherited size is not counted as its own step.

const seen = new Map();
for (const el of document.querySelectorAll('*')) {
  if (el.children.length || !el.textContent?.trim()) continue;
  const s = getComputedStyle(el);
  const key = `${parseFloat(s.fontSize)}px  w${s.fontWeight}  lh ${s.lineHeight}  ls ${s.letterSpacing}`;
  seen.set(key, (seen.get(key) ?? 0) + 1);
}
[...seen].sort((a, b) => b[1] - a[1]);

Sorted by usage, so the body size is first and the one-offs last. Derive the ratio between adjacent sizes. A consistent ratio means a scale; scattered values mean hard-coded sizes.

The spacing rhythm

const vals = new Map();
for (const el of document.querySelectorAll('*')) {
  const s = getComputedStyle(el);
  for (const p of ['paddingTop', 'paddingLeft', 'marginTop', 'marginLeft', 'gap', 'rowGap']) {
    const v = parseFloat(s[p]);
    if (v > 0) vals.set(v, (vals.get(v) ?? 0) + 1);
  }
}
[...vals].sort((a, b) => a[0] - b[0]);

Look for the base unit that divides most values, then check better-layout's grouping rule. Is the gap between groups at least 2× the gap within one?

Radii, shadows, borders

const grab = (prop, skip) => {
  const m = new Map();
  for (const el of document.querySelectorAll('*')) {
    const v = getComputedStyle(el)[prop];
    if (v && v !== skip) m.set(v, (m.get(v) ?? 0) + 1);
  }
  return [...m].sort((a, b) => b[1] - a[1]);
};
({ radius: grab('borderRadius', '0px'), shadow: grab('boxShadow', 'none') });

Count the distinct shadows. One or two recipes is a system; nine is a page where everyone invented their own elevation.

Motion

const t = new Map();
for (const el of document.querySelectorAll('*')) {
  const s = getComputedStyle(el);
  if (s.transitionDuration === '0s') continue;
  const key = `${s.transitionProperty}  ${s.transitionDuration}  ${s.transitionTimingFunction}`;
  t.set(key, (t.get(key) ?? 0) + 1);
}
[...t].sort((a, b) => b[1] - a[1]);

transition: all shows up here as all. Custom curves arrive as cubic-bezier(...), and the built-in keywords tell you nobody tuned them.

Breakpoints

const bp = new Set(); const unreadable = [];
for (const sheet of document.styleSheets) {
  let rules; try { rules = sheet.cssRules } catch { unreadable.push(sheet.href); continue }
  const walk = list => { for (const r of list ?? []) {
    if (r.media) { for (const m of r.media) { const hit = m.match(/(min|max)-width:\s*([\d.]+)(px|r?em)/); if (hit) bp.add(hit[0]) } }
    if (r.cssRules) walk(r.cssRules);
  }};
  walk(rules);
}
({ breakpoints: [...bp].sort(), unreadable });

Compare against the framework defaults. Breakpoints at exactly 640/768/1024/1280 are Tailwind's out of the box, which tells you they were never chosen.

Fonts and theming

({
  loaded: [...document.fonts].map(f => `${f.family} ${f.weight} ${f.style} ${f.status}`),
  bodyStack: getComputedStyle(document.body).fontFamily,
  variable: [...document.fonts].some(f => String(f.weight).includes(' ')),
  themeClass: document.documentElement.className || '(none)',
  colorScheme: getComputedStyle(document.documentElement).colorScheme,
});

variable: true means one file covers a weight range. A class on <html> beside a prefers-color-scheme query means a toggle that can override the system, the mechanism better-colors describes.

Reading a second state

Everything above reads one state at one width. Before writing the explanation, at minimum:

  • Resize to 375px and re-run the spacing and breakpoint snippets. The values that change are what is fluid.
  • Toggle the theme and re-run the token snippet. The tokens that change are the themed layer, the ones that do not are the primitives.
  • Tab to the first interactive control and read its :focus-visible styles, since a focus ring is one of the most common absences.

Source: SKILL.md on GitHub

1 warning1mo3 checks · Risk SAFE
  • Gen Agent Trust Hub1mo

    This skill provides a framework for analyzing and explaining website interfaces from URLs or screenshots. It uses standard browser instrumentation and shell commands to inspect publicly accessible front-end code (HTML, CSS, JS) and presents findings to the user. No malicious code or exfiltration patterns were detected.

  • Socket1mo

    No alerts

  • Snyk1mo

    Risk: MEDIUM · 1 issue

Signed by skilld at 267330e. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last month.

Steadyupdated last month
disable-model-invocation
true

README badge

README badge for jakubkrehel/skills/explain-interface