WordPress UX/Design Enforcement
Definitive standards for building WordPress sites that are fast, accessible, and visually consistent. Every rule below is enforceable in code review.
1. Core Web Vitals for WordPress
LCP (Largest Contentful Paint) < 2.5s
The hero image or heading is almost always the LCP element. Prioritize it explicitly.
<!-- Preload the hero image in <head> -->
<link rel="preload" as="image" href="/wp-content/uploads/hero.webp"
fetchpriority="high" type="image/webp">
<!-- Mark the hero img element -->
<img src="hero.webp" alt="Hero banner" fetchpriority="high"
width="1280" height="720" decoding="async">
WordPress-specific: disable lazy-load on the first image via filter.
// functions.php — skip lazy-load on above-fold images
add_filter( 'wp_img_tag_add_loading_attr', function( $value, $image, $context ) {
if ( str_contains( $image, 'hero-banner' ) ) {
return false; // no loading="lazy"
}
return $value;
}, 10, 3 );
CLS (Cumulative Layout Shift) < 0.1
Every replaced element MUST have explicit dimensions.
/* Reserve space for images before load */
img, video, iframe {
max-width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
/* Prevent font-swap layout shift */
@font-face {
font-family: 'Brand';
src: url('brand.woff2') format('woff2');
font-display: swap;
size-adjust: 105%; /* match fallback metrics */
ascent-override: 95%;
}
/* Reserve ad/embed space */
.ad-slot { min-height: 250px; }
.embed-container { aspect-ratio: 16 / 9; }
INP (Interaction to Next Paint) < 200ms
// Debounce expensive scroll/resize handlers
function debounce(fn, ms = 150) {
let id;
return (...args) => { clearTimeout(id); id = setTimeout(() => fn(...args), ms); };
}
window.addEventListener('scroll', debounce(handleScroll), { passive: true });
// Break long tasks with yield
async function processItems(items) {
for (const item of items) {
doWork(item);
if (performance.now() - start > 50) {
await new Promise(r => setTimeout(r, 0)); // yield to main thread
}
}
}
Keep DOM under 1500 nodes. Audit with: document.querySelectorAll('*').length.
2. Mobile-First WordPress Design
Breakpoint Strategy
/* Mobile-first: base styles are mobile (320px+) */
/* Small phones handled by fluid units, no breakpoint needed */
@media (min-width: 480px) { /* Large phones */ }
@media (min-width: 768px) { /* Tablets */ }
@media (min-width: 1024px) { /* Small desktop */ }
@media (min-width: 1280px) { /* Large desktop */ }
Touch Targets and Viewport
/* Minimum 44x44px touch targets — WCAG 2.5.8 */
button, a, input, select, textarea {
min-height: 44px;
min-width: 44px;
}
/* Prevent iOS zoom on input focus */
input, select, textarea {
font-size: 16px; /* >= 16px prevents auto-zoom */
}
<meta name="viewport" content="width=device-width, initial-scale=1">
Mobile Menu Patterns
Hamburger menu for primary nav on mobile. Place critical actions in thumb zone (bottom 40% of screen).
/* Bottom nav for high-frequency actions */
.mobile-bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 56px;
display: flex;
justify-content: space-around;
align-items: center;
background: var(--wp--preset--color--base);
box-shadow: 0 -1px 3px rgb(0 0 0 / 0.1);
z-index: 100;
padding-bottom: env(safe-area-inset-bottom);
}
@media (min-width: 768px) {
.mobile-bottom-nav { display: none; }
}
3. Typography System
Modular Scale (ratio 1.25 — Major Third)
:root {
--step--2: clamp(0.64rem, 0.58rem + 0.28vw, 0.80rem);
--step--1: clamp(0.80rem, 0.73rem + 0.35vw, 1.00rem);
--step-0: clamp(1.00rem, 0.91rem + 0.43vw, 1.25rem); /* body */
--step-1: clamp(1.25rem, 1.14rem + 0.54vw, 1.56rem); /* h4 */
--step-2: clamp(1.56rem, 1.43rem + 0.68vw, 1.95rem); /* h3 */
--step-3: clamp(1.95rem, 1.78rem + 0.85vw, 2.44rem); /* h2 */
--step-4: clamp(2.44rem, 2.23rem + 1.07vw, 3.05rem); /* h1 */
}
WordPress theme.json Typography
{
"settings": {
"typography": {
"fluid": true,
"fontSizes": [
{ "slug": "small", "size": "clamp(0.80rem, 0.73rem + 0.35vw, 1.00rem)", "name": "Small" },
{ "slug": "medium", "size": "clamp(1.00rem, 0.91rem + 0.43vw, 1.25rem)", "name": "Medium" },
{ "slug": "large", "size": "clamp(1.56rem, 1.43rem + 0.68vw, 1.95rem)", "name": "Large" },
{ "slug": "x-large","size": "clamp(2.44rem, 2.23rem + 1.07vw, 3.05rem)", "name": "Extra Large" }
],
"fontFamilies": [
{ "slug": "brand", "fontFamily": "'Brand', system-ui, sans-serif", "name": "Brand" },
{ "slug": "mono", "fontFamily": "'JetBrains Mono', monospace", "name": "Mono" }
]
}
}
}
Line Length and Spacing
/* Optimal measure: 45-75 characters */
.entry-content p,
.entry-content li {
max-width: 65ch;
line-height: 1.6;
}
h1, h2, h3 { line-height: 1.2; }
h4, h5, h6 { line-height: 1.3; }
Font Loading Strategy
// Preload critical fonts in <head>
add_action( 'wp_head', function() {
echo '<link rel="preload" href="' . get_theme_file_uri('fonts/brand.woff2')
. '" as="font" type="font/woff2" crossorigin>' . "\n";
}, 1 );
4. Color System
theme.json Color Palette
{
"settings": {
"color": {
"palette": [
{ "slug": "primary", "color": "#1a56db", "name": "Primary" },
{ "slug": "secondary", "color": "#6b7280", "name": "Secondary" },
{ "slug": "accent", "color": "#f59e0b", "name": "Accent" },
{ "slug": "base", "color": "#ffffff", "name": "Base" },
{ "slug": "contrast", "color": "#111827", "name": "Contrast" },
{ "slug": "success", "color": "#059669", "name": "Success" },
{ "slug": "warning", "color": "#d97706", "name": "Warning" },
{ "slug": "error", "color": "#dc2626", "name": "Error" }
]
}
}
}
Semantic Token Usage
/* Use WordPress preset variables everywhere */
.btn-primary {
background: var(--wp--preset--color--primary);
color: var(--wp--preset--color--base);
}
.alert-error {
border-left: 4px solid var(--wp--preset--color--error);
background: color-mix(in srgb, var(--wp--preset--color--error) 8%, white);
}
WCAG AA Contrast (4.5:1 text, 3:1 large text/UI)
/* Dark mode via media query */
@media (prefers-color-scheme: dark) {
:root {
--wp--preset--color--base: #111827;
--wp--preset--color--contrast: #f9fafb;
}
}
Always verify contrast ratios. Minimum 4.5:1 for body text, 3:1 for large text (18px+ or 14px bold) and UI components.
5. Navigation UX
Breadcrumbs
// Output semantic breadcrumbs (works with Yoast, Rank Math, or custom)
function zentratec_breadcrumbs() {
if ( function_exists('rank_math_the_breadcrumbs') ) {
rank_math_the_breadcrumbs();
} elseif ( function_exists('yoast_breadcrumb') ) {
yoast_breadcrumb('<nav aria-label="Breadcrumb">', '</nav>');
}
}
Pagination: Prefer Numbered Over Infinite Scroll
Infinite scroll breaks footer access and disables back-button history. Use numbered pagination or "Load More" with URL state.
// Accessible numbered pagination
the_posts_pagination([
'mid_size' => 2,
'prev_text' => '<span aria-label="Previous page">«</span>',
'next_text' => '<span aria-label="Next page">»</span>',
]);
Back-to-Top
.back-to-top {
position: fixed;
bottom: 2