# Design, UX & Accessibility for CMS Themes
Platform-agnostic guidance for building beautiful, inclusive, and performant CMS themes. Applies to Hugo, WordPress, Ghost, Statamic, Craft CMS, Jekyll, Eleventy — any system that renders templates to HTML.
---
## Table of Contents
1. [Typography Systems](#1-typography-systems)
2. [Accessible Color & Contrast](#2-accessible-color--contrast)
3. [Spacing & Layout](#3-spacing--layout)
4. [Design Tokens & Theming](#4-design-tokens--theming)
5. [Semantic HTML & Landmarks](#5-semantic-html--landmarks)
6. [ARIA & Dynamic Content](#6-aria--dynamic-content)
7. [Keyboard Navigation & Focus](#7-keyboard-navigation--focus)
8. [Accessible Forms & Search](#8-accessible-forms--search)
9. [Content-First Design Patterns](#9-content-first-design-patterns)
10. [Navigation & Information Architecture](#10-navigation--information-architecture)
11. [Engagement Patterns](#11-engagement-patterns)
12. [Performance & Core Web Vitals](#12-performance--core-web-vitals)
13. [Modern CSS for Themes](#13-modern-css-for-themes)
14. [Theme Testing & QA](#14-theme-testing--qa)
15. [Sources & References](#15-sources--references)
---
## 1. Typography Systems
### 1.1 Fluid Type Scale
A modular type scale ensures visual harmony across headings and body text. Use `clamp()` to size fluidly between viewport widths without media queries:
```css
:root {
--step--2: clamp(0.6944rem, 0.6515rem + 0.2144vw, 0.8333rem);
--step--1: clamp(0.8333rem, 0.7708rem + 0.3125vw, 1rem);
--step-0: clamp(1rem, 0.9115rem + 0.4427vw, 1.25rem);
--step-1: clamp(1.2rem, 1.0755rem + 0.6224vw, 1.5625rem);
--step-2: clamp(1.44rem, 1.2665rem + 0.8671vw, 1.9531rem);
--step-3: clamp(1.728rem, 1.4885rem + 1.1979vw, 2.4414rem);
--step-4: clamp(2.074rem, 1.7466rem + 1.6372vw, 3.0518rem);
--step-5: clamp(2.488rem, 2.0463rem + 2.2084vw, 3.8147rem);
}
h1 { font-size: var(--step-5); }
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
body { font-size: var(--step-0); }
small { font-size: var(--step--1); }
```
**Typography best practices:**
- Body text: 16–18px (1rem–1.125rem) as base
- Line height: 1.5–1.7 for body, 1.1–1.3 for headings
- Measure (line length): 45–75 characters per line, ideal 66 CPL. WCAG 1.4.8 (AAA) mandates max 80 CPL.
- Use `ch` units for text container width: `max-width: 65ch`
- Limit to 2–3 font families and 3–4 weights total
### 1.2 Font Loading Strategy
Self-host fonts as WOFF2 for performance and privacy:
```css
@font-face {
font-family: 'BodyFont';
src: url('/fonts/body-regular.woff2') format('woff2');
font-display: swap; /* Show fallback text immediately */
font-weight: 400;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,
U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
U+FEFF, U+FFFD;
}
```
```html
```
Never use `font-display: block` — it hides text for up to 3 seconds while the font loads, creating a flash of invisible text (FOIT). Use `swap` to render fallback text immediately, or `optional` if you'd rather fall back to the system font entirely on slow connections.
### 1.3 WCAG Text Spacing (SC 1.4.12)
Users may override your text spacing for readability. Ensure no content loss when these overrides are applied:
```css
.prose p {
line-height: 1.5; /* minimum 1.5× font size */
margin-bottom: 1.5em; /* 1.5× spacing between paragraphs */
word-spacing: 0.16em;
letter-spacing: 0.12em;
}
```
The WCAG text spacing bookmarklet applies these overrides — test with it during development.
---
## 2. Accessible Color & Contrast
### 2.1 WCAG Contrast Requirements
**WCAG 2.2 minimum ratios (SC 1.4.3 & 1.4.6):**
| Level | Normal text | Large text (18pt+ / 14pt bold) | UI components & graphics |
|-------|-------------|--------------------------------|--------------------------|
| AA | ≥ 4.5:1 | ≥ 3:1 | ≥ 3:1 (SC 1.4.11) |
| AAA | ≥ 7:1 | ≥ 4.5:1 | n/a |
### 2.2 Designing an Accessible Palette
Choose color tokens that meet contrast from the start — don't fix them later:
```css
:root {
/* Text — all ≥8.6:1 on white */
--color-text-primary: #1a1a1a; /* 15:1 on white */
--color-text-secondary: #4a4a4a; /* 8.6:1 on white */
/* Use muted text sparingly — it must still be readable */
--color-text-muted: #6b6b6b; /* 5.2:1 on white — small text minimum */
/* Brand colors with accessible contrast on their expected backgrounds */
--color-primary: #0055cc; /* 4.8:1 on white, passes AA for text */
--color-primary-text: #ffffff; /* for buttons on --color-primary */
/* Surface */
--color-surface: #ffffff;
--color-surface-secondary: #f5f5f5; /* Sufficient contrast from white for borders */
--color-border: #d4d4d4;
}
```
**Hard rules:**
- Never convey information by color alone — add icons, underlines, or text labels
- Links must have ≥ 3:1 contrast from body text AND an underline OR hover/focus underline
- Test all color pairs with WebAIM contrast checker or axe DevTools before shipping
- Test with `prefers-contrast: more` — a user preference for increased contrast
### 2.3 Dark Mode
```css
@media (prefers-color-scheme: dark) {
:root {
--color-surface: #1a1a2e;
--color-text-primary: #e8e8e8;
--color-text-secondary: #a0a0a0;
--color-border: #2a2a3e;
--color-link: #6ba3ff;
--color-link-hover: #8bb9ff;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.3);
}
}
/* Manual toggle override */
[data-theme="dark"] {
--color-surface: #1a1a2e;
--color-text-primary: #e8e8e8;
/* ... same overrides ... */
}
```
**Modern browsers** support `light-dark()` for simpler theme switching (Chrome 123+, Firefox 128+):
```css
:root {
color-scheme: light dark;
--color-surface: light-dark(#ffffff, #1a1a2e);
--color-text-primary: light-dark(#1a1a1a, #e8e8e8);
--color-link: light-dark(#0055cc, #6ba3ff);
}
```
### 2.4 Theme Switching Without Flash
Apply the user's preferred theme before any CSS renders to prevent a flash of incorrect theme:
```html
```
Place this inline in `
` before any stylesheets. It blocks rendering for microseconds but prevents the jarring light-to-dark flash.
---
## 3. Spacing & Layout
### 3.1 Consistent Spacing Scale
Base on a 4px or 8px unit:
```css
:root {
--space-0: 0;
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-5: 1.5rem; /* 24px */
--space-6: 2rem; /* 32px */
--space-7: 3rem; /* 48px */
--space-8: 4rem; /* 64px */
--space-9: 6rem; /* 96px */
}
```
**Vertical rhythm** — consistent spacing between elements without thinking about each one:
```css
/* CUBE CSS flow utility */
.flow > * + * {
margin-top: var(--flow-space, 1em);
}
```
### 3.2 Content-Out Page Layout
A content-first grid that gives you full-bleed and constrained regions without nested wrappers:
```css
.page-layout {
display: grid;
grid-template-columns:
[full-start] minmax(1rem, 1fr)
[main-start] minmax(0, 65ch)
[main-end] minmax(1rem, 1fr)
[full-end];
}
.page-layout > * {
grid-column: main-start / main-end; /* All children default to content column */
}
.page-layout > .full-width {
grid-column: full-start / full-end; /* Opt in to full bleed */
}
```
### 3.3 Responsive Card Grid
No media queries needed:
```css
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(300px, 100%), 1fr));
gap: var(--space-5);
}
```
**Responsive testing guidance:**
- Use content-driven breakpoints, not device-driven ones
- Test at every 100px width from 320px to 1600px
- WCAG 1.4.10 (Reflow) requires no horizontal scroll at 320px equivalent width
- Consider container queries for component-level responsiveness
---
## 4. Design Tokens & Theming
### 4.1 Token Architecture
Layer tokens in a hierarchy: **Global → Semantic → Component**
```css
/* Layer 1: Raw values (seldom change) */
:root {
--color-blue-600: #0055cc;
--color-blue-700: #003d99;
--font-body: 'Inter', system-ui, sans-serif;
--font-heading: 'Inter', system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Cascadia Code', monospace;
}
/* Layer 2: Semantic tokens (theme-aware) */
:root {
--color-surface: #ffffff;
--color-text: #1a1a1a;
--color-link: var(--color-blue-600);
--color-link-hover: var(--color-blue-700);
--spacing-section: 4rem;
--border-radius-sm: 4px;
--border-radius-md: 8px;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
}
/* Layer 3: Component-level overrides (in component CSS files) */
.card {
--card-padding: var(--space-4);
--card-radius: var(--border-radius-md);
}
```
### 4.2 User Preference Detection
Always respect these user preferences:
```css
/* High contrast */
@media (prefers-contrast: more) {
:root {
--color-text: #000000;
--color-text-secondary: #1a1a1a;
--color-border: #000000;
}
}
/* Reduced transparency */
@media (prefers-reduced-transparency: reduce) {
* {
opacity: 1 !important;
backdrop-filter: none !important;
}
}
```
---
## 5. Semantic HTML & Landmarks
### 5.1 Page Landmarks (WCAG 1.3.1)
Every CMS theme should provide these landmark regions:
```html
Skip to main content
Page Title
```
### 5.2 Heading Hierarchy (WCAG 1.3.1)
- One `
` per page (usually the page/post title in CMS)
- Heading levels must not skip (h1 → h2 → h3, never h1 → h3)
- For CMS themes: ensure editors can't break hierarchy — provide visual guidance in the editor, or use a render hook that maps heading levels to a semantic hierarchy
```html