Skip to main content

Design Quality

What separates a page that works from one that merely exists?

Every criterion below has a measurable threshold. Pass/fail, not opinion. Run this as a protocol, not a suggestion.

Sources: WCAG 2.2, Nielsen's 10 Heuristics, Google Core Web Vitals, Apple HIG, Material Design 3, USWDS Design Tokens, Butterick's Practical Typography


Rendering

Before evaluating quality, confirm the design renders at all. This gate must pass before any other check runs.

#CheckThresholdHow to Measure
1.1Every CSS token resolves to a non-empty value0 unresolved tokensgetComputedStyle(el).getPropertyValue('--token') must not be empty
1.2Every bg-* class produces a visible computed valueNo rgba(0,0,0,0) on elements with semantic bg classesgetComputedStyle(el).backgroundColor on every .bg-* element
1.3Every text color differs from its background0 invisible text elementsCompare color to nearest ancestor backgroundColor
1.4Framework base styles don't override utility classes on h1-h6, a, buttonComputed color matches utility class intentCreate element with class, compare getComputedStyle to token value
1.5All images/SVGs render with visible dimensionsWidth and height > 0, no broken iconsgetBoundingClientRect() on every img and svg
1.6No elements overflow creating hidden content0 unintended horizontal scrollbarsscrollWidth <= clientWidth

Contrast

#CheckThresholdHow to Measure
2.1Body text contrast ratio>= 4.5:1 (AA)Luminance ratio from color and backgroundColor RGB
2.2Large text (>= 18.66px bold or >= 24px) contrast>= 3:1 (AA)Same calculation with size threshold
2.3UI component contrast (borders, icons, forms)>= 3:1Per WCAG 1.4.11 Non-text Contrast
2.4Minimum body text size>= 16px desktop, >= 15px mobilegetComputedStyle(el).fontSize on p elements
2.5Minimum label/caption text>= 12pxSmallest text on page
2.6No informational elements below 0.5 opacityDecorative only below 0.5Check opacity on elements with text or meaningful SVG
2.7Body font weight>= 400 (Regular)Avoid Ultralight/Thin/Light for body
2.8Link distinction from body textUnderline OR contrast >= 3:1 + non-color indicatorPer WCAG 1.4.1 Use of Color

Type Scale

#CheckThresholdHow to Measure
3.1Consistent scale ratioSteps follow a ratio within +/- 10% (e.g. 1.25 Major Third)Compute sizes[n+1] / sizes[n] for all sizes
3.2Maximum distinct sizes<= 6 per pageCount unique fontSize values
3.3Heading hierarchyH1 > H2 > H3 in DOM order, no skipped levelsParse heading tags in DOM order
3.4Same semantic level = same sizeAll H2s same size (+/- 1px), all H3s sameGroup by tag, check variance
3.5Heading-to-body ratioH1: 2x-4x body. H2: 1.4x-2.5x bodyh1Size / bodySize
3.6Body line-height1.4-1.8x font-sizeparseFloat(lineHeight) / parseFloat(fontSize)
3.7Heading line-height1.1-1.3x font-sizeSame on h1, h2, h3
3.8Line length (measure)45-75 charactersel.clientWidth / (parseFloat(fontSize) * 0.5)
3.9Letter-spacing on uppercase text0.05em-0.2emgetComputedStyle(el).letterSpacing on text-transform: uppercase
3.10Font families<= 3 distinctCount unique fontFamily values
3.11Eyebrow size relative to body>= 70% of body (body 18px = label >= 13px)Compare label to body size

Spacing

#CheckThresholdHow to Measure
4.1Values align to base unit>= 80% multiples of 4px or 8pxAudit all margin/padding
4.2Gap after heading < gap between sectionsheading.marginBottom < section.paddingTopDirect comparison
4.3Related elements closer than unrelated (Gestalt)Intra-group < inter-group by >= 2xCompare space-y within groups vs padding between
4.4Same-level sections use same paddingVariance <= 8pxGroup by type, compare padding
4.5No dead zones > 150pxMax whitespace gap <= 150pxBottom of section N to top of section N+1
4.6Heading-to-body gap consistencySame gap (+/- 4px) everywhereheading.bottom to nextSibling.top across sections
4.7First-screen content densityHero content >= 40% of viewporttotalContentHeight / viewportHeight
4.8Repeated patterns identical spacingCards, rows: internal spacing variance = 0Compare across all instances
4.9Mobile padding scales downMobile <= 66% of desktopCompare at 375px vs 1280px

Visual Flow

#CheckThresholdHow to Measure
5.1Primary message above foldH1 + value prop in first viewportrect.bottom < viewportHeight
5.2One focal point per viewport-heightNo two elements of equal visual weight competeIdentify largest/boldest at each scroll position
5.3Narrative section orderHook, Framework, Evidence, ActionMap sections to roles; CTA after value established
5.4Every section earns its placeNo redundancy between sectionsIf two sections merge without loss, they should
5.5Clear section boundariesBackground change, divider, or >= 64px gapVisual differentiator between adjacent sections
5.6CTA visibilityHighest-contrast, largest interactive elementCompare CTA contrast/size to all other interactive elements
5.7Progressive disclosureComplexity increases as user scrollsWord count section 1 < section N
5.8Scannable structureEyebrow + heading + <= 50 words before detailCheck intro length before lists/tables

Responsive

#CheckThresholdHow to Measure
6.14 breakpoints tested375px, 768px, 1024px, 1440px without breakageScreenshot or programmatic test
6.2No mobile horizontal overflowscrollWidth <= clientWidth at 375pxProgrammatic check
6.3Mobile touch targetsAll interactive elements >= 44x44pxgetBoundingClientRect() at mobile width
6.4Font scalingH1 <= 48px at 375px, body stableComputed sizes per breakpoint
6.5Media scales proportionallyAspect ratio maintained, fits viewportNo overflow or distortion
6.6Cumulative Layout ShiftCLS <= 0.1Lighthouse or Web Vitals API
6.7400% zoom reflowAll content accessible without horizontal scrollPer WCAG 1.4.10

Interactive

#CheckThresholdHow to Measure
7.1Links/buttons distinct from static textColor, underline, or border distinguishesCan a new user tell what's clickable?
7.2Hover state presentAll interactive elements change on hoverTest :hover produces visible difference
7.3Focus ring visibleAll elements show focus indicator on TabTab through page
7.4Active/pressed stateButtons show feedback on clickTest :active state
7.5Disabled state distinctVisually different from enabledCompare computed styles
7.6Consistent patternsAll links styled same, all buttons styled sameNo link looks like button without reason
7.7Navigation at every breakpointMenu accessible at all 4 breakpointsTest hamburger opens/closes, links reachable

Performance

#CheckThresholdHow to Measure
8.1Largest Contentful Paint<= 2.5sLighthouse
8.2Interaction to Next Paint<= 200msLighthouse
8.3Cumulative Layout Shift<= 0.1Lighthouse
8.4Total page weight<= 1MB initial loadNetwork tab transfer size
8.5Font loadingNo FOIT > 100msfont-display: swap; test throttled
8.6Image optimizationModern format (WebP/AVIF), correct dimensionsCheck src formats and rendered vs natural size

Accessibility

#CheckThresholdHow to Measure
9.1All images have alt text0 img without altDOM audit
9.2SVG descriptionsInformational SVGs have role="img" + aria-labelDOM audit
9.3Valid heading structureExactly one H1, no skipped levelsParse heading elements
9.4Landmark regionsmain, nav, footer presentDOM audit
9.5Color not sole differentiatorNo info conveyed by color aloneCheck color-coded elements
9.6Keyboard navigableAll elements reachable via TabManual test
9.7Reduced motion respectedAnimations honor prefers-reduced-motionCheck CSS @media rule exists
9.8Language declaredhtml lang="en" presentDOM check

Content

#CheckThresholdHow to Measure
10.1Value proposition lengthHero heading <= 12 wordsWord count
10.2Self-explanatory subheadingsH2s tell the story without body textRead H2s in order
10.3No orphan sectionsEvery section has heading + bodyDOM audit
10.4Consistent voiceSame person and tense throughoutManual review
10.5Action-oriented CTAButton text contains a verbCheck primary CTA
10.6No dead-end pages>= 1 forward navigation pathDOM audit for internal links

Audit Script

Run in browser console after any visual change. Returns pass/fail for each measurable criterion.

(function designAudit() {
const results = { pass: 0, fail: 0, issues: [] };

function check(id, name, passed, detail) {
if (passed) { results.pass++; }
else { results.fail++; results.issues.push({ id, name, detail }); }
}

// 1.2 Background tokens render
document.querySelectorAll('[class*="bg-"]').forEach(el => {
const cls = Array.from(el.classList).find(c =>
c.startsWith('bg-') && !c.startsWith('bg-gradient')
);
if (cls && !['bg-white', 'bg-transparent', 'bg-inherit'].includes(cls)) {
const bg = getComputedStyle(el).backgroundColor;
if (bg === 'rgba(0, 0, 0, 0)') {
check('1.2', 'BG token renders', false,
cls + ' resolves to transparent on <' + el.tagName + '>');
}
}
});

// 1.3 + 2.1 Text visibility and contrast
function luminance(r, g, b) {
const [rs, gs, bs] = [r, g, b].map(c => {
c = c / 255;
return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
});
return 0.2126 * rs + 0.7152 * gs + 0.0722 * bs;
}

function parseRGB(str) {
const m = str.match(/(\d+),\s*(\d+),\s*(\d+)/);
return m ? [+m[1], +m[2], +m[3]] : null;
}

function contrastRatio(c1, c2) {
const l1 = luminance(...c1), l2 = luminance(...c2);
const lighter = Math.max(l1, l2), darker = Math.min(l1, l2);
return (lighter + 0.05) / (darker + 0.05);
}

document.querySelectorAll('h1,h2,h3,p,span,a,li').forEach(el => {
if (!el.textContent.trim()) return;
const color = parseRGB(getComputedStyle(el).color);
const bgEl = el.closest('[class*="bg-"]') || document.body;
let bg = parseRGB(getComputedStyle(bgEl).backgroundColor);
if (!bg || (bg[0] === 0 && bg[1] === 0 && bg[2] === 0 &&
getComputedStyle(bgEl).backgroundColor.includes('0)'))) {
bg = [255, 255, 255];
}
if (color && bg) {
const ratio = contrastRatio(color, bg);
const size = parseFloat(getComputedStyle(el).fontSize);
const weight = parseInt(getComputedStyle(el).fontWeight);
const isLarge = size >= 24 || (size >= 18.66 && weight >= 700);
const threshold = isLarge ? 3 : 4.5;
if (ratio < threshold) {
check('2.1', 'Contrast', false,
el.tagName + ' "' + el.textContent.trim().substring(0, 30) +
'" ratio=' + ratio.toFixed(1) + ' need ' + threshold + ':1');
}
}
});

// 3.4 Same heading level = same size
const headingSizes = {};
document.querySelectorAll('main h1, main h2, main h3').forEach(h => {
const tag = h.tagName;
const size = getComputedStyle(h).fontSize;
if (!headingSizes[tag]) headingSizes[tag] = new Set();
headingSizes[tag].add(size);
});
Object.entries(headingSizes).forEach(([tag, sizes]) => {
check('3.4', tag + ' consistent size', sizes.size <= 1,
tag + ' has ' + sizes.size + ' sizes: ' + [...sizes].join(', '));
});

// 4.5 No dead zones > 150px
const sections = document.querySelectorAll('main > section');
for (let i = 1; i < sections.length; i++) {
const prevBottom = sections[i - 1].getBoundingClientRect().bottom;
const currTop = sections[i].getBoundingClientRect().top;
const gap = currTop - prevBottom;
if (gap > 150) {
check('4.5', 'No dead zones', false,
gap + 'px gap between section ' + (i - 1) + ' and ' + i);
}
}

// 9.1 Images have alt text
document.querySelectorAll('main img').forEach(img => {
check('9.1', 'Image alt text', img.hasAttribute('alt'),
'Missing alt on img src="' + img.src.substring(0, 50) + '"');
});

// 9.3 Heading structure
const headings = [...document.querySelectorAll(
'main h1,main h2,main h3,main h4,main h5,main h6'
)];
const h1Count = headings.filter(h => h.tagName === 'H1').length;
check('9.3', 'Single H1', h1Count === 1, 'Found ' + h1Count + ' H1 elements');

// Summary
console.log('\n=== DESIGN AUDIT ===');
console.log('PASS: ' + results.pass + ' | FAIL: ' + results.fail);
if (results.issues.length) {
console.log('\nISSUES:');
results.issues.forEach(i =>
console.error('[' + i.id + '] ' + i.name + ': ' + i.detail)
);
} else {
console.log('All checks passed.');
}
return results;
})();

How This Works

ConceptApplication
StandardThis checklist defines "good design" in measurable terms
ProtocolThe audit script and manual checks are methods that recreate expected outcomes
BenchmarkA page passes with 0 FAIL results across all 10 categories
TriggerAny FAIL triggers a fix workflow before the task completes

Context