The scanner is one file. The audit data is a CSV. Re-run it yourself.
VHPunk is a deterministic static analysis. Three rules. No AI, no ML, no heuristics. Same input → same output, every time. Bring your own codebase.
The three rules
- vh literal in a height property. Any of
height,min-height,max-height,block-size,min-block-size,max-block-sizewith a value matching/(-?\d+(?:\.\d+)?)\s*vh\b/— for exampleheight: 100vh,min-height: 90vh,max-height: 50vh. - vh threshold ≥ 50. Small
vhvalues (under 50vh — typical spacers likeheight: 2vh) are not flagged. The iOS Safari URL-bar overlap problem is a 100vh-scale problem; small VHs aren't the trap. - No
dvh/svh/lvhfallback nearby. If the same declaration block (or an adjacent@supportsbranch) already uses one of the dynamic viewport units, the finding is suppressed — you already handled it.
What gets scanned
The scanner recursively walks a directory and parses these file types:
.html,.htm— both<style>blocks andstyle="..."inline attributes.jsx,.tsx,.astro— including JSX object-literal styles (style={{ height: "100vh" }}).vue,.svelte— embedded style blocks.css,.scss— parsed with PostCSS
Skipped by default: node_modules, dist, build, .next, .cache.
Scoring
Score = max(40, 100 - findings × 8). Floor at 40 so a heavily-traveled site doesn't bottom out and lose all signal. A clean codebase = 100. Five findings = 60. Twenty findings = 40 (the floor).
Run it yourself · 60 seconds
$ npx --yes @radioart/vhpunk ./src # scan a single file $ npx --yes @radioart/vhpunk ./src/HeroSection.tsx # machine-readable output (CI gates, dashboards) $ npx --yes @radioart/vhpunk ./src --format=json # GitHub Actions annotations $ npx --yes @radioart/vhpunk ./src --format=github
Free for any project, any size, any commercial use. The --fix flag (in-place patches) requires a $99 lifetime license — pay once, use forever.
The receipts
Below is the actual scanner source and the raw audit data we ran for this writeup. Click to download. The CSV is small enough to read inline; the scanner is one file, ~250 lines, MIT-licensed.
scan(targetPath). Returns an array of findings. ~250 lines, MIT.What the audit found · honest read
Honest read. Most modern marketing-site homepages no longer hardcode 100vh inline — the rendered HTML the edge serves is mostly clean. That's because the bug has migrated into component bundles. The hero gets cut off behind iOS Safari's URL bar in production because of a single height: 100vh in a React/Vue/Svelte component that ships in the client JS bundle, not because the marketing site's <style> block has it.
This is exactly why VHPunk is a source-code scanner, not a rendered-HTML scanner. The free in-browser scan at /scan catches what's in the rendered HTML; npx @radioart/vhpunk catches what's in your source tree, which is where the bugs actually live.
The fixtures at the bottom of the CSV (buggy-landing.html, buggy-react.jsx, clean-landing.html) prove the scanner detects what it claims to detect — known-state inputs, predictable outputs.
What VHPunk does not scan
- Computed styles after JavaScript runs (we scan source, not runtime DOM).
- Tailwind / Atomic CSS classes like
h-screen. The Tailwind preset uses100vhin its underlying CSS, so the fix happens upstream in yourtailwind.config.js— VHPunk won't catch the class itself, but a follow-up Punk (TailwindPunk, on the roadmap) will. - Shadow DOM contents (a known limitation we'll lift once
jsdom's shadow support matures). - External CSS files referenced via
<link rel="stylesheet">from a remote URL. Run the CLI in your repo where the source CSS lives.
dvh-equivalent behavior and the spec for dvh/svh/lvh shipped in Safari 16 in 2023. We're three years past the fix and the trap is still in production. ← back to vhpunk.pages.dev