Get the FREE Ultimate OpenClaw Setup Guide →

wp-ux-design

Scanned
npx machina-cli add skill xonack/wp-ux-design-claude-skill/wp-ux-design --openclaw
Files (1)
SKILL.md
20.1 KB

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">&laquo;</span>',
    'next_text' => '<span aria-label="Next page">&raquo;</span>',
]);

Back-to-Top

.back-to-top {
    position: fixed;
    bottom: 2rem;
    right: 2rem;
    opacity: 0;
    transition: opacity 200ms ease;
    pointer-events: none;
}
.back-to-top.visible {
    opacity: 1;
    pointer-events: auto;
}
const btn = document.querySelector('.back-to-top');
const observer = new IntersectionObserver(([e]) => {
    btn.classList.toggle('visible', !e.isIntersecting);
}, { rootMargin: '-300px 0px 0px 0px' });
observer.observe(document.querySelector('header'));

6. Page Builder UX

Section / Row / Column Hierarchy

All builders (Tatsu, Elementor, WPBakery) share this pattern. Enforce consistent spacing at each level.

/* Consistent section spacing */
.tatsu-section,
.elementor-section,
.vc_section {
    padding-block: var(--section-spacing, clamp(3rem, 6vw, 6rem));
}

/* Content width management */
.tatsu-row,
.elementor-container,
.vc_row {
    max-width: var(--content-width, 1200px);
    margin-inline: auto;
    padding-inline: clamp(1rem, 3vw, 2rem);
}

Builder CSS Override Pattern

Use specificity, not !important. Target the builder's own wrapper classes.

/* Override builder defaults cleanly with specificity */
body .tatsu-section .tatsu-column .tatsu-text-block p {
    font-size: var(--step-0);
    line-height: 1.6;
    max-width: 65ch;
}

/* For Elementor: use the widget wrapper */
.elementor-widget-text-editor .elementor-widget-container p {
    font-size: var(--step-0);
}

7. Image Optimization UX

Responsive Images with Art Direction

<picture>
    <source media="(min-width: 768px)"
            srcset="wide-800.webp 800w, wide-1200.webp 1200w, wide-1600.webp 1600w"
            sizes="(min-width: 1280px) 1200px, 100vw"
            type="image/webp">
    <source srcset="square-400.webp 400w, square-600.webp 600w"
            sizes="100vw" type="image/webp">
    <img src="fallback-800.jpg" alt="Descriptive alt text"
         width="800" height="600" loading="lazy" decoding="async">
</picture>

WordPress Lazy Loading Rules

  • Above-fold images: NO loading="lazy", YES fetchpriority="high"
  • Below-fold images: YES loading="lazy", YES decoding="async"
  • Background images: use content-visibility: auto on the container

Placeholder Strategy (LQIP)

// Generate inline low-quality placeholder
function get_lqip_style( $attachment_id ) {
    $thumb = wp_get_attachment_image_url( $attachment_id, [32, 32] );
    if ( ! $thumb ) return '';
    return sprintf(
        'background: url(%s) center/cover no-repeat; filter: blur(20px);',
        esc_url( $thumb )
    );
}
<div class="img-wrapper" style="<?php echo get_lqip_style($id); ?>">
    <img src="full.webp" loading="lazy" decoding="async"
         onload="this.parentElement.style.background='none'"
         width="800" height="600" alt="Photo description">
</div>

8. Form UX

Inline Validation and Smart Defaults

<form novalidate>
    <div class="field-group">
        <label for="email">Email address</label>
        <input id="email" type="email" name="email" required
               autocomplete="email" inputmode="email"
               aria-describedby="email-error"
               pattern="[^@]+@[^@]+\.[a-zA-Z]{2,}">
        <p id="email-error" class="field-error" role="alert" hidden>
            Enter a valid email address
        </p>
    </div>
</form>
// Inline validation on blur, not on input
document.querySelectorAll('input[required]').forEach(input => {
    input.addEventListener('blur', () => {
        const error = document.getElementById(input.getAttribute('aria-describedby'));
        if (!error) return;
        const invalid = !input.validity.valid;
        error.hidden = !invalid;
        input.setAttribute('aria-invalid', String(invalid));
    });
});

Multi-Step Forms

/* Progress indicator */
.form-steps {
    display: flex;
    gap: 0.5rem;
    counter-reset: step;
}
.form-step { counter-increment: step; }
.form-step::before {
    content: counter(step);
    display: grid;
    place-items: center;
    width: 2rem;
    height: 2rem;
    border-radius: 50%;
    background: var(--wp--preset--color--secondary);
    color: white;
    font-weight: 700;
}
.form-step[aria-current="step"]::before {
    background: var(--wp--preset--color--primary);
}

Autocomplete Attributes Checklist

Always set: name, email, tel, street-address, postal-code, country, cc-number, cc-exp, cc-csc, given-name, family-name, organization.


9. Loading States

Skeleton Screens Over Spinners

Skeletons preserve layout and feel faster than spinners. Use them for content regions.

.skeleton {
    background: linear-gradient(90deg,
        var(--wp--preset--color--base) 25%,
        color-mix(in srgb, var(--wp--preset--color--contrast) 8%, transparent) 50%,
        var(--wp--preset--color--base) 75%);
    background-size: 200% 100%;
    animation: shimmer 1.5s infinite;
    border-radius: 4px;
}
@keyframes shimmer { to { background-position: -200% 0; } }

.skeleton-text { height: 1em; margin-bottom: 0.75em; }
.skeleton-text:last-child { width: 60%; }
.skeleton-image { aspect-ratio: 16 / 9; }

Transition Timing

  • Micro-interactions (hover, focus): 150ms
  • Content transitions (modals, accordions): 250ms
  • Page-level transitions: 300ms
  • Easing: cubic-bezier(0.4, 0, 0.2, 1) for standard, cubic-bezier(0, 0, 0.2, 1) for deceleration

WordPress AJAX Pattern

async function wpAjaxLoad(action, container) {
    container.setAttribute('aria-busy', 'true');
    container.innerHTML = '<div class="skeleton skeleton-text"></div>'.repeat(3);
    try {
        const res = await fetch(wpApiSettings.root + action, {
            headers: { 'X-WP-Nonce': wpApiSettings.nonce }
        });
        if (!res.ok) throw new Error(res.statusText);
        container.innerHTML = await res.text();
    } catch (err) {
        container.innerHTML = '<p class="error-message">Failed to load. <button onclick="wpAjaxLoad(\'' + action + '\', this.closest(\'[aria-busy]\'))">Retry</button></p>';
    } finally {
        container.removeAttribute('aria-busy');
    }
}

10. Error States

404 Page Requirements

Every 404 must include: (1) clear "not found" message, (2) search form, (3) popular/recent content links, (4) link back to homepage.

// 404.php template
get_header(); ?>
<main class="error-404" role="main">
    <h1><?php esc_html_e('Page not found', 'theme'); ?></h1>
    <p><?php esc_html_e('The page you requested does not exist or has moved.', 'theme'); ?></p>
    <?php get_search_form(); ?>
    <h2><?php esc_html_e('Popular pages', 'theme'); ?></h2>
    <ul>
    <?php
    $popular = new WP_Query(['posts_per_page' => 5, 'orderby' => 'comment_count']);
    while ($popular->have_posts()) : $popular->the_post(); ?>
        <li><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></li>
    <?php endwhile; wp_reset_postdata(); ?>
    </ul>
    <a href="<?php echo esc_url(home_url('/')); ?>"><?php esc_html_e('Back to homepage', 'theme'); ?></a>
</main>
<?php get_footer();

Form Error Recovery

Never clear valid fields on error. Scroll to the first error. Announce errors to screen readers with role="alert".

Graceful Degradation

// Fallback when external service fails
function get_external_data() {
    $cached = get_transient('external_data');
    if ($cached !== false) return $cached;

    $response = wp_remote_get('https://api.example.com/data', ['timeout' => 5]);
    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        $stale = get_option('external_data_fallback', []);
        return $stale; // serve stale data rather than fail
    }

    $data = json_decode(wp_remote_retrieve_body($response), true);
    set_transient('external_data', $data, HOUR_IN_SECONDS);
    update_option('external_data_fallback', $data);
    return $data;
}

11. WordPress Admin UX

Custom Settings Pages

Match WordPress admin styling. Use the Settings API.

add_action('admin_menu', function() {
    add_options_page('Brand Settings', 'Brand', 'manage_options', 'brand-settings', 'render_brand_settings');
});

add_action('admin_init', function() {
    register_setting('brand_group', 'brand_primary_color', ['sanitize_callback' => 'sanitize_hex_color']);
    add_settings_section('brand_colors', 'Color Settings', null, 'brand-settings');
    add_settings_field('primary_color', 'Primary Color', function() {
        $val = get_option('brand_primary_color', '#1a56db');
        echo '<input type="color" name="brand_primary_color" value="' . esc_attr($val) . '">';
    }, 'brand-settings', 'brand_colors');
});

function render_brand_settings() {
    echo '<div class="wrap"><h1>Brand Settings</h1><form method="post" action="options.php">';
    settings_fields('brand_group');
    do_settings_sections('brand-settings');
    submit_button();
    echo '</form></div>';
}

Admin Notices

// Dismissible success notice
add_action('admin_notices', function() {
    if (!get_transient('brand_saved')) return;
    delete_transient('brand_saved');
    echo '<div class="notice notice-success is-dismissible"><p>Settings saved.</p></div>';
});

Use notice-success, notice-error, notice-warning, notice-info. Always add is-dismissible for non-critical messages.


12. Performance UX Checklist

Run this checklist before any page is considered complete.

MetricTargetHow to Verify
LCP< 2.5sLighthouse, CrUX, web-vitals JS lib
CLS< 0.1Lighthouse, Layout Instability API
INP< 200msCrUX, web-vitals JS lib
Above-fold render< 1sWebPageTest filmstrip
Time to Interactive< 3sLighthouse
DOM nodes< 1500document.querySelectorAll('*').length
Font displayNo FOIT/FOUTNetwork tab, font-display: swap
Scroll performance60fpsDevTools Performance panel
Touch response< 100msINP measurement
Image dimensionsAll setimg:not([width]) selector audit
Lazy loadingBelow-fold onlyVerify first image has no lazy attr
Critical CSS inlinedAbove-fold CSSView source, check <style> in head

Quick Audit Script

// Paste in DevTools console
(() => {
    const imgs = document.querySelectorAll('img:not([width]), img:not([height])');
    const bigDOM = document.querySelectorAll('*').length;
    const noAlt = document.querySelectorAll('img:not([alt])');
    const smallTargets = [...document.querySelectorAll('a, button')].filter(el => {
        const r = el.getBoundingClientRect();
        return r.width < 44 || r.height < 44;
    });
    console.table({
        'Images missing dimensions': imgs.length,
        'DOM nodes': bigDOM,
        'Images missing alt': noAlt.length,
        'Touch targets < 44px': smallTargets.length,
    });
})();

Enforcement Rules

When reviewing WordPress code, flag violations of these standards:

  1. No dimensions on images -- always require width/height attributes
  2. lazy loading on above-fold images -- first viewport image must not be lazy
  3. Touch targets below 44px -- buttons and links must meet minimum size
  4. Missing autocomplete attributes on forms -- all identity/payment fields need them
  5. Spinner instead of skeleton -- prefer skeleton screens for content regions
  6. Infinite scroll without URL state -- pagination must preserve browser history
  7. !important in builder overrides -- use specificity instead
  8. Hardcoded colors instead of CSS custom properties -- use semantic tokens
  9. Missing error states -- every async operation needs failure handling
  10. No font-display on @font-face -- always set font-display: swap

Source

git clone https://github.com/xonack/wp-ux-design-claude-skill/blob/main/skills/wp-ux-design/SKILL.mdView on GitHub

Overview

Defines definitive, code-reviewable rules for fast, accessible WordPress sites with visual consistency. It covers Core Web Vitals, mobile-first layout, typography, color systems, navigation, page builder patterns, image optimization, form UX, loading and error states, admin UX, and practical performance checklists with CSS/HTML/PHP examples.

How This Skill Works

Rules are codified into actionable patterns and snippets that teams apply during code reviews. It provides concrete HTML, CSS, and PHP examples to enforce LCP, CLS, and INP targets, mobile-first breakpoints, accessible touch targets, and WordPress-specific optimizations (e.g., preloading hero images and disabling lazy-load for above-fold imagery). The skill also includes admin and form UX guidance to keep interfaces consistent and performant.

When to Use It

  • During a WordPress site build or redesign to meet Core Web Vitals and visual standards
  • In code reviews to enforce hero preloads, image sizing, and lazy-loading rules
  • When implementing mobile-first breakpoints, touch targets, and viewport meta tags
  • While configuring admin UX, forms UX, and loading/error states
  • During performance audits and ongoing optimization using the provided CSS/HTML/PHP examples

Quick Start

  1. Step 1: Add a hero image preload and explicit dimensions; disable lazy-load for the hero in WP where appropriate
  2. Step 2: Implement mobile-first CSS, set a valid viewport meta, and ensure touch targets are at least 44px
  3. Step 3: Review admin/Form UX and run a Core Web Vitals check, applying the provided HTML/CSS/PHP patterns

Best Practices

  • Preload hero assets and disable first-image lazy-loading via WP filters where appropriate
  • Measure LCP, CLS, and INP; reserve space with explicit dimensions and stable fonts
  • Use mobile-first CSS with progressive enhancement and clear breakpoints
  • Maintain a consistent color system and typography scale across components
  • Audit admin UI and forms for loading states and error handling; keep the DOM lean

Example Use Cases

  • Hero image preloading and explicit width/height attributes to boost LCP
  • Using a WordPress filter to disable lazy-load on above-fold hero images
  • CSS techniques like aspect-ratio and font-display: swap to stabilize CLS
  • Mobile-first breakpoint strategy with 320px base and 768px+ breakpoints
  • Bottom mobile navigation pattern for high-frequency actions and thumb reach

Frequently Asked Questions

Add this skill to your agents
Sponsor this space

Reach thousands of developers