Use when tightening a site's visual system: too many font sizes, one-off colours, spacing, radii, button heights or icon sizes, or when someone says hairy styles, design units, count the units, or shot units. Measures every unit on the rendered page or the whole site, at phone and desktop, night and day; boxes each stray (the values a hair off) on a screenshot with the file and line that sets it; and plans how to condense the rest.
Permissions
Files
SKILL.md
hairy-styles
Use when tightening a site's visual system: too many font sizes, one-off colours, spacing, radii, button heights or icon sizes, or when someone says hairy styles, design units, count the units, or shot units. Measures every unit on the rendered page or the whole site, at phone and desktop, night and day; boxes each stray (the values a hair off) on a screenshot with the file and line that sets it; and plans how to condense the rest.
user-invocable
true
hairy-styles
Every font size, weight, family, colour, spacing value, radius, border, shadow, control height and icon size is a design unit. Do the most with the fewest, and add a unit only when contrast has to explain a function. (Evan Conrad, Design Advice for Programmers.) The hairy ones are a hair off: 15px next to 14px, #a3a3a3 next to #a1a1a1.
5 files · 74 KB5 KB
Version history
Run
node <this skill's folder>/scripts/units.mjs <url|port|dir> one page
node <this skill's folder>/scripts/units.mjs <url> --crawl 12 the site: every nav page, plus one page per template
node <this skill's folder>/scripts/units.mjs <url> /pricing /docs the pages you name
A folder (dist/, out/) is served locally. It needs Playwright (npm i -g playwright && npx playwright install chromium). With the shot screenshot CLI, shot units is the same command.
Around a UI change, measure before and after. Only what the change added comes back, each stray with the file and line that sets it, boxed in .shots/units/after/:
shot before and shot after do this on their own (--no-units skips it).
Flags: --views phone,desktop, --themes night,day, --click <selector> to measure an open sheet, --wait <ms>, --repo <dir> to find a live site's strays in its source, --out <dir> (default .shots/units), --json, --base <units.json> (only what changed since that run), --quiet (one line, no screenshots).
Each page is scrolled to the bottom first, so lazy and reveal-on-scroll content counts once it settles. Hidden and screen-reader-only text doesn't count.
Read it
The table: distinct units per view, across every page measured. * marks more than 6 type sizes, 3 weights or 3 families (the gtm-ui bars).
The values: every unit with its element count, merged where views match.
The strays, numbered to match the boxes on the screenshots. Open the images before fixing anything.
near X: a hair from a unit that's used more (1px for type and spacing, 2px for radii, controls and icons, colour ΔE under 0.02). Nearly always a typo. Merge it.
once, try X: one element uses it; X is the closest unit in use.
once: one element, nothing close. Keep it only if it marks a function: the page headline, a selected state, a price going down.
↳ file:line: where it's written, found with git grep in the repo around a local page, or in --repo.
The plan (condense): every value mapped to the unit it should become, with the count before and after. Type sizes snap to the standard scale (12, 14, 16, 18, 20, 24, 30, 36, 48 and up), spacing to the 4px grid, and weights to 400, 500 and 700. Colours, radii, control heights and icon sizes merge into a near-identical value that is used more.
Not counted: colours and sizes that come from data: set in a style attribute, or different on every one of a row of same-class siblings (heat-map cells, chart bars). Leave them.
Decide once: .hairy-styles.json
It never asks. A deliberate one-off goes in .hairy-styles.json at the repo root, and it stops coming back as a stray for everyone. A scale makes the plan snap onto your own system instead of the defaults. For a gtm-ui site:
The units are sizes, weights, families, colors, spacing, radii, borders, shadows, controls and icons.
Fix
Work the plan at the source: the token, class or variable, never one element. text-[15px] becomes text-sm; two near greys become one variable. The ↳ lines say where.
Fix every sibling on every route, in both themes.
Snap toward the more-used unit unless the other is the system's own value (gtm-ui's ladder is 12, 14, 20, 30, 56, icons are 16, and weights are 400, 500, 700).
Weights: bold is 700. Snap 600 up to 700 and never 700 down to 600, even when 600 is used more; 600 next to 500 reads as a mistake, not a step.
Don't touch content: user text, embeds, images, data tints.
Re-run, then shot after to check nothing moved. Report before and after:
Unit
Before
After
Type sizes
9
6
Strays
30
4
In CI
ci/hairy-styles.yml is a GitHub Actions workflow. On each pull request it builds the base branch and the PR, measures both, and keeps one comment on the PR up to date with only the units and strays the PR added. Copy it to .github/workflows/, copy scripts/units.mjs to .github/hairy-styles/units.mjs, and set the build command and output folder at the top.