WCAG Compliance Checklist for Headless Frontend Builds

This checklist turns Accessibility Compliance in Headless Frontends into a build pipeline. WCAG compliance in a headless build is won or lost before the client renders: when validation is deferred to the browser, semantic contracts break and unstructured rich-text HTML from editors fails screen-reader parsing. This checklist shifts enforcement left — into the API, serialization, and CI/CD layers — with production-ready patterns for WCAG 2.2 AA across decoupled frontends.

The five checkpoints of the checklistContent passes API validation, deterministic serialization, media fallbacks, preview parity checks and CI audits before reaching production, each checkpoint owning one class of WCAG failures.1. API validationheadings, names2. Serializationsemantic HTML3. Mediaalt, captions4. Preview paritylang, live regions5. CI auditsaxe, LighthouseProduction
Each numbered section below corresponds to one checkpoint in this pipeline.

1. API-layer schema validation and payload sanitization

Accessibility starts before the build runs. Headless content models let authors bypass structural requirements, so enforce JSON Schema validation at the CMS gateway or in the data-fetching layer. Configure middleware to reject or quarantine payloads that violate core rules:

  • Missing or out-of-order heading hierarchies (h1h6)
  • Absent aria-label or aria-labelledby on interactive components
  • Unlabeled form controls or missing fieldset/legend groupings
  • Inline styles that override prefers-reduced-motion or prefers-contrast

A pre-render hook parses responses against the schema; on violation, fail the build or quarantine the payload to staging rather than patching client-side. Draft content gets the same scrutiny as production, per Preview & Draft Workflow Patterns.

TypeScript
// Pre-render validation hook for CMS payloads
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const a11ySchema = {
  type: 'object',
  required: ['headingHierarchy', 'interactiveElements'],
  properties: {
    headingHierarchy: {
      type: 'array',
      items: { type: 'number', minimum: 1, maximum: 6 },
      description: 'Sequential heading levels must not skip more than one step'
    },
    interactiveElements: {
      type: 'array',
      items: {
        type: 'object',
        required: ['accessibleName'],
        properties: {
          accessibleName: { type: 'string', minLength: 1 }
        }
      }
    }
  }
};

export function validateAccessibilityPayload(data: Record<string, unknown>): boolean {
  const ajv = new Ajv({ allErrors: true });
  addFormats(ajv);
  const validate = ajv.compile(a11ySchema);
  const isValid = validate(data);

  if (!isValid) {
    console.error('[A11Y] Payload validation failed:', validate.errors);
    return false;
  }
  return true;
}

2. Deterministic rich-text serialization

CMS rich text serializes as nested JSON nodes. Mapping them to accessible HTML needs deterministic transformation, or server and client diverge and you get hydration mismatches — usually from logic that depends on runtime state or non-deterministic DOM queries.

Use a recursive serializer that enforces semantic mapping and propagates ARIA attributes through the tree, handling omitted captions, alt text, and landmark roles. Cache outputs by a deterministic hash (e.g. SHA-256 of the normalized node tree) to suppress hydration warnings in Next.js or Nuxt.

TypeScript
// Deterministic rich-text serializer with ARIA enforcement
interface CMSNode {
  type: string;
  content?: string;
  children?: CMSNode[];
  attributes?: Record<string, string>;
  id?: string;
  level?: number;
  alt?: string;
}

export function serializeNode(node: CMSNode, context: { depth: number } = { depth: 0 }): string {
  switch (node.type) {
    case 'image': {
      if (!node.alt || node.alt.trim() === '') {
        // Decorative fallback per WCAG 1.1.1
        return `<img src="${node.attributes?.src}" role="presentation" aria-hidden="true" loading="lazy" />`;
      }
      return `<figure>
        <img src="${node.attributes?.src}" alt="${escapeHtml(node.alt)}" loading="lazy" />
        ${node.attributes?.caption ? `<figcaption>${escapeHtml(node.attributes.caption)}</figcaption>` : ''}
      </figure>`;
    }
    case 'heading': {
      const level = Math.min(Math.max(node.level || 2, 1), 6);
      return `<h${level} id="${node.id || `heading-${context.depth}`}">${node.content || ''}</h${level}>`;
    }
    case 'blockquote': {
      const cite = node.attributes?.cite ? ` cite="${escapeHtml(node.attributes.cite)}"` : '';
      return `<blockquote${cite}><p>${node.content || ''}</p></blockquote>`;
    }
    case 'list': {
      const tag = node.attributes?.ordered ? 'ol' : 'ul';
      const items = node.children?.map(child => `<li>${serializeNode(child, { depth: context.depth + 1 })}</li>`).join('') || '';
      return `<${tag}>${items}</${tag}>`;
    }
    default: {
      return `<div role="region" aria-label="Content block">${node.content || ''}</div>`;
    }
  }
}

function escapeHtml(str: string): string {
  return str.replace(/[&<>"']/g, m => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[m] || m));
}

3. Media pipeline and asset compliance

Headless media APIs often return unoptimized URLs that skip responsive generation. Configure your transformation API (Cloudinary, Imgix, or next/image) to enforce alt text, loading strategy, and viewport-aware sizes at the edge. Intercept missing alt during asset resolution:

  • Informative media, missing alt: render with alt="" so screen readers skip it rather than read a placeholder, and flag the asset for editorial review; a generic label such as “image description pending” conveys nothing and misleads users.
  • Decorative assets: strip alt and apply aria-hidden="true" to cut screen-reader verbosity.
  • Responsive breakpoints: generate srcset and sizes from your spacing scale, with max-width: 100% and height: auto to prevent layout shifts that disrupt focus order.
TypeScript
// CDN edge transformation interceptor
export function generateImageProps(src: string, alt?: string, sizes: string = '100vw') {
  const isDecorative = !alt || alt.trim() === '';
  return {
    src,
    alt: isDecorative ? undefined : alt,
    role: isDecorative ? 'presentation' : undefined,
    'aria-hidden': isDecorative ? 'true' : undefined,
    loading: 'lazy',
    decoding: 'async',
    sizes,
    width: 800,
    height: 600
  };
}

4. Preview environments and draft context

Draft workflows add auth tokens, unoptimized asset paths, and isolated rendering contexts that bypass accessibility middleware. When wiring Accessibility Compliance in Headless Frontends, make the preview iframe or server route inherit the same ARIA context as production.

Token-based preview often strips lang attributes, injects debug styles, or suppresses aria-live regions during HMR. Return raw content alongside a sanitized accessibility manifest, and verify draft state management doesn’t break focus order, skip landmarks, or disable keyboard traps. Concretely:

  1. Mirror production lang and dir attributes in preview routes.
  2. Preserve aria-live="polite" and aria-atomic states during draft hydration.
  3. Disable non-essential animation toggles in preview to respect prefers-reduced-motion.
  4. Route preview focus through a central focus manager, and trap focus only inside modal dialogs; the preview itself must never trap the keyboard (WCAG 2.1.2).

5. CI/CD integration and automated auditing

Manual reviews don’t scale across decoupled architectures. Run axe-core, pa11y, or Lighthouse CI against static routes and dynamic preview endpoints in the pipeline:

  • Audit a representative sample of templates (home, article, product, form).
  • Fail builds on critical and serious violations; warn on moderate.
  • Emit JSON reports that map violations back to CMS content IDs for fast remediation.
  • Cache results to skip unchanged routes.
Audit coverage by pipeline stageWhich accessibility checks run at each stage of the delivery pipeline, from pull requests to scheduled production crawls.Pull requestcode defectseslint-plugin-jsx-a11yaxe on componentsPreview / stagingtemplate + content defectsaxe on page templatesLighthouse CIProductioncontent driftsitemap crawlRUM error reports
Fast deterministic checks gate merges; content-dependent checks run where real content exists.
YAML
# .github/workflows/a11y-audit.yml
name: Accessibility Compliance Check
on: [push, pull_request]
jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build
      - run: npx lighthouse-ci autorun --config=lighthouserc.json
      - name: Fail on critical violations
        if: failure()
        run: echo "Accessibility threshold breached. Review Lighthouse CI report."
WCAG 2.2 AA criteria most affected by CMS contentWCAG success criteria that CMS content most often breaks, the headless layer responsible and how the checklist enforces them.CriterionTypical CMS causeEnforced at1.1.1 Non-text contentmissing alt textAPI validation + media fallback1.3.1 Info and relationshipsskipped heading levelsserializer with relative levels1.4.3 Contrast (minimum)editor-chosen colourspalette tokens + build check2.4.4 Link purpose"click here" linksCMS validation warning3.1.2 Language of partsfallback-locale passageslang from resolved locale
Most content-driven failures fall into these criteria; each has a checkpoint that stops it.

Configuration Reference

Setting Value Why
axe tags wcag2a, wcag2aa, wcag21aa, wcag22aa Matches the WCAG 2.2 AA target.
Fail threshold critical and serious Blocks real barriers without drowning in noise.
Template sample home, listing, article, form, search Covers the component combinations that exist.
Report mapping violation to CMS entry id Content fixes go to editors directly.
Crawl schedule weekly over the sitemap Catches content drift between deploys.

Gotchas & Edge Cases

  • Serializer output without escaping. The serializer above interpolates node.content into HTML without escaping in several branches. Escape all editor-supplied text, or render through a framework that escapes by default, to avoid both broken markup and injection.
  • Generic region wrappers. Defaulting unknown nodes to role="region" with the same label creates many identically named landmarks, which clutters screen reader navigation. Render unknown nodes as plain containers and log them.
  • Lighthouse as the only audit. Lighthouse runs a subset of axe rules on a single page load. Add axe with the full WCAG tag set on the templates that matter.
  • Checklists that nobody reruns. A checklist is only useful if it runs on every change. Encode each item as a test, a lint rule or a CMS validation, and keep the manual items few and explicit.

Worked Example

A university’s headless site had passed an external audit at launch and failed the next one a year later, with 140 issues, almost all introduced by content: images without alt text, heading levels skipped inside rich text, and links reading “more”. The team applied this checklist: required alt fields with a decorative flag, a serializer with relative heading levels, a CMS warning on generic link text, and a weekly crawl that mapped violations to entry ids and sent them to the responsible editors. Six months later, the crawl reported fewer than ten open issues at any time, each with an owner, and the follow-up audit found no content-related failures.

Frequently Asked Questions

Does passing automated audits mean the site is WCAG compliant?

No. Automated tools detect only a portion of WCAG failures. The checklist’s manual items, keyboard walk-throughs and screen reader passes on new components, cover what automation cannot see.

How often should the checklist run?

The automated parts on every pull request and every deploy, the crawl weekly, and the manual items whenever a component is added or significantly changed.

Should editors see audit results?

Yes, for content-caused issues. Mapping violations to entry ids and sending them to the editors who own those entries fixes problems at the source and teaches the patterns that avoid them.

Which part of the checklist gives the most value first?

API-level validation of alt text and heading structure, because it prevents the most common content-caused failures at the moment editors create them, before any code or audit is involved.

How does the checklist handle third-party embeds from the CMS?

Treat embeds, such as videos, maps and forms, as components with their own checks: require titles on iframes, captions on video, and an accessible alternative where the embed itself is not accessible.

Where can editors report accessibility problems they notice?

Give the preview banner a short “report an accessibility issue” link that opens a form prefilled with the page and entry id. Editors notice problems daily; a one-click report turns those observations into tracked issues instead of hallway remarks.