# 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

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

Post Title

Introduction

Sub-point

``` ### 5.3 Proper `