Benchmarks · No. 01
Readability.
The first Brndle benchmark. A measurable standard for typography and reading flow that every Brndle reference (WordPress, Astro, Ghost, Shopify) must hit, regardless of how the runtime spells out its template syntax. Below: the ten rules, the target numbers, the cross-stack implementation, and a checklist you can run your own theme through.
What we mean by "readable"
Readable, here, means measurable.
"Readable" is one of those words a theme developer can use without ever committing to a number. We refuse that. For the purposes of this benchmark, a theme is readable when ten specific measurements all clear the bar at the same time, in both light and dark mode, on a real device.
The measurements are not opinions. They are a working consensus from a century of typography research applied to screens, distilled into ten rules that you can verify with a ruler, a contrast checker, and a fresh device profile. If a theme fails three or more, we do not call it a Brndle reference, no matter how good the design system or the build tooling.
The Brndle theme for WordPress passes all ten. The Astro, Ghost, and Shopify references in progress are being held to the same ten. The same is true of any theme you ship under the Brndle name in the future.
The ten rules
Each rule, with the Brndle target value.
These are not the only readability concerns - they are the ones that matter at theme level. Per-page typography choices (article-specific drop caps, run-in heads, sidebars) live one layer above this benchmark.
-
Type scale: ratio 1.250 (minor third), six steps minimum
Pick one modular ratio. Stick to it. Brndle uses 1.250 because it gives strong hierarchy without dramatic display sizes. Six steps cover everything you need: caption, body, lede, h3, h2, h1.
Target 1.250 modular · 6 named steps · no off-scale sizes
-
Measure: 60-75 characters per line on body prose
Comfortable reading lives between 60 and 75 characters per line. Wider than 75 and the eye loses the next-line anchor. Narrower than 50 and the eye breaks too often. Brndle targets 65ch on the article body, capped via max-width on the prose container.
Target max-width: 65ch on .prose · single-column body
-
Line-height: 1.6 for body, 1.1-1.2 for display
Display sizes need tight line-height (1.05-1.2) so multi-line h1s read as one shape. Body needs generous line-height (1.55-1.7) for scan + return. Lists tighten one notch (1.4-1.5).
Target body 1.6 · display 1.15 · lists 1.45
-
Base size: 16px floor on mobile, 17-18px on desktop
Below 16px on mobile, iOS zooms forms - and your readers squint. Above 18px on desktop, the page feels like an ebook. Brndle uses 16px base on mobile, 17px on tablet, 18px on desktop via clamp().
Target font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem)
-
Color contrast: WCAG 2.2 AA on body, AAA on critical UI
Body text must hit 4.5:1 contrast against its background. Large text (18.66px+ regular OR 14px+ bold) must hit 3:1. Critical UI (form labels, error states, primary CTAs) goes for AAA (7:1) when reasonable. Test in both light and dark mode.
Target 4.5:1 body · 3:1 large · 7:1 critical UI
-
Variable fonts only. One file per family.
No theme should ship eight static font weight files when one variable file covers 100-900. Variable fonts cut HTTP requests, total transferred bytes, and CLS during font load. Self-host the woff2 from same origin to skip Google Fonts as a render-blocking external request.
Target 1 woff2 per family · self-hosted · font-display: swap
-
Font loading: preload critical, swap, never block render
Preload the woff2 of the immediately-visible weight (body 400 + display 800 typically). Use font-display: swap so the system fallback paints first. The variable axis means the fallback shape is close enough that CLS is minimal.
Target <link rel="preload" as="font" type="font/woff2"> on critical files · font-display: swap on all
-
Paragraph rhythm: 1em vertical gap, no first-paragraph indent
Web typography is web typography, not print. No first-line indent. Vertical spacing of 1em (or 0.75em on tighter UI surfaces) between paragraphs. Headings get more space above than below (a 2:1 ratio reads as a section break).
Target <p> margin-bottom: 1em · <h2-h6> margin-top: 1.5em margin-bottom: 0.5em
-
Letter-spacing: tightened on large display, default on body
Large display type (h1, h2 above 32px) wants negative tracking to feel set rather than spelled. Body prose at 16-18px wants 0 tracking - any change is overkill. Mono / caps labels get +0.05em to +0.12em.
Target display: -0.02em to -0.04em · body: 0 · mono caps: +0.08em
-
Reading mode: dark + light + system, no exceptions
Every Brndle reference ships a dark mode that is not the inverse of light mode. Background should be a true dark color (slate-950 / true black is overkill on most screens), body text should be slate-300 or warmer, accents should brighten one step. Tested in both modes for contrast.
Target html.dark class only · localStorage persisted · no prefers-color-scheme override on saved choice
Cross-stack implementation
The same target, expressed in four runtimes.
Each runtime spells out the type tokens differently. The numbers do not change. WordPress uses Tailwind v4's @theme block via Sage. Astro uses :root variables in tokens.css. Ghost uses CSS custom properties referenced from default.hbs. Shopify reads from settings_data.json via Liquid output.
WordPress (Sage + Tailwind v4)
/* tokens.css - shipped by the Brndle WordPress theme */
@theme {
--font-display: 'Inter Variable', system-ui, sans-serif;
--font-body: 'Inter Variable', system-ui, sans-serif;
--font-mono: 'JetBrains Mono Variable', monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-md: 1rem;
--text-lg: 1.25rem;
--text-xl: 1.5625rem;
--text-2xl: 1.953rem;
--text-3xl: 2.441rem;
--text-4xl: 3.052rem;
--leading-tight: 1.15;
--leading-snug: 1.35;
--leading-relaxed: 1.6;
--tracking-tighter: -0.04em;
--tracking-tight: -0.02em;
--tracking-wide: 0.05em;
--tracking-wider: 0.08em;
} Astro (.astro + tokens.css)
/* src/styles/tokens.css imported via Layout.astro */
:root {
--font-display: 'Inter Variable', system-ui, sans-serif;
--font-body: 'Inter Variable', system-ui, sans-serif;
--font-mono: 'JetBrains Mono Variable', monospace;
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--leading-body: 1.6;
--leading-display: 1.15;
--measure: 65ch;
}
body { font: var(--text-base) / var(--leading-body) var(--font-body); }
.prose { max-width: var(--measure); } Ghost (default.hbs + theme.css)
/* assets/css/tokens.css linked in default.hbs */
:root {
--gh-font-body: 'Inter Variable', system-ui, sans-serif;
--gh-text-base: clamp(16px, 1rem + 0.2vw, 18px);
--gh-leading: 1.6;
--gh-measure: 65ch;
}
.gh-post-content {
font: var(--gh-text-base) / var(--gh-leading) var(--gh-font-body);
max-width: var(--gh-measure);
margin-inline: auto;
} Shopify (theme.css + OS 2.0 sections)
/* assets/base.css - settings.font_body is the OS 2.0 font picker */
:root {
--font-body: {{ settings.font_body.family }}, system-ui, sans-serif;
--text-base: {{ settings.font_size_base | default: '16px' }};
--leading: 1.6;
--measure: 65ch;
}
.rte, .article__body, .product__description {
font: var(--text-base) / var(--leading) var(--font-body);
max-width: var(--measure);
} Self-check
Run your theme through the ten.
Score one point per rule that clears the target. Less than seven, the theme is not yet meeting the Brndle readability bar. Seven to nine, it is most of the way - the remaining items are usually a font-loading fix or a contrast pass away. Ten, ship it.
- ☐ One modular type scale (state the ratio and the named steps)
- ☐ Body prose capped at 60-75 characters per line via max-width on the container
- ☐ Body line-height between 1.55 and 1.7; display between 1.05 and 1.20
- ☐ Mobile base font-size at least 16px (test with actual mobile zoom off)
- ☐ WCAG 2.2 AA contrast on body text in both light and dark mode
- ☐ Variable fonts only, self-hosted woff2, font-display: swap
- ☐ Preload tag on the immediately-visible weight (body 400, display 800 typically)
- ☐ Paragraph vertical rhythm: 1em gap, no first-line indent
- ☐ Display type with negative tracking (-0.02 to -0.04em), body at 0, mono caps with positive tracking
- ☐ Dark mode tested for contrast (not just an inverted light mode)
Further reading
Notes + sources we lean on.
- Robert Bringhurst, The Elements of Typographic Style
- The canonical printed-text reference. Sections on measure (8.1), leading (2.2), and proportional spacing (3.2.3) apply directly to screens.
- WCAG 2.2 - Contrast (Minimum) 1.4.3 + Contrast (Enhanced) 1.4.6
- The accessibility spec for text contrast. AA for the body, AAA where reasonable. w3.org/WAI/WCAG22
- Tim Brown, Modular Scale
- The practical case for picking one modular ratio and using only its named steps. modularscale.com
- Google Fonts - Variable Fonts
- Why one variable woff2 is smaller than four static weights for almost every real theme. fonts.google.com/knowledge
- web.dev - Avoid invisible text during font loading
- The case for font-display: swap and the trade-off with CLS. web.dev/avoid-invisible-text
Up next
More benchmarks in the series.
Readability is benchmark No. 01. Each of the topics below gets the same long-form, cross-stack treatment. The numbered slots are reserved; the writeups land in order.
- 02Accessibility. WCAG 2.2 AA + AAA, keyboard nav, ARIA landmarks, focus indicators, reduced-motion, color-blind safe palettes.
- 03Performance. Lighthouse 100 baseline, KB JS budget, font loading strategy, image formats (AVIF/WebP), CLS/LCP/INP targets.
- 04Semantics. HTML5 structure, heading hierarchy, landmark roles, schema.org markup, microdata vs JSON-LD.
- 05Dark mode. Proper color-scheme handling, system preference vs explicit choice, contrast preservation in both modes.
- 06Responsive. Fluid type scales, breakpoints vs container queries, real-content responsive testing.