Payload CMS Accessibility Checklist 2026 | WCAG 2.1 AA & EAA
Last updated: 2026-05-25
Payload is a TypeScript-native, code-first headless CMS that has become a popular choice for teams building on Next.js and the modern React stack. Because it is headless, Payload itself does not render your public website - it stores structured content and exposes it through a REST and GraphQL API (and, in the Next.js App Router setup, via local APIs), and your own front-end code decides what HTML reaches the browser. That architecture moves almost all accessibility responsibility onto the rendering layer the developer writes, which is both an opportunity and a trap. The opportunity is total control: nothing forces div soup or inaccessible widgets on you. The trap is that there is no theme or page builder quietly handling landmarks, headings, focus, or labels, so anything you do not build accessibly simply is not. The recurring failure points are specific to how Payload content is shaped. Rich text is stored as a Lexical (or, in older projects, Slate) JSON tree, and teams write a serializer that converts that tree to React/HTML; a naive serializer drops heading semantics, emits links with no discernible text, or wraps everything in divs. The Blocks field lets editors compose a page from arbitrary block types, and each block's React component is responsible for its own headings, alt text, and interactive semantics - one carelessly built block (a hero, an accordion, a tabbed section, a carousel) becomes a site-wide accessibility bug. The Upload collection stores media but only enforces an alt field if you add and require it, so informative images ship without descriptions when the schema does not demand them. Navigation is often built from a relationship or array field, and the front end has to render it as a real nav landmark with keyboard-operable submenus. Forms (via the Form Builder plugin or a custom collection) need real labels and error association that the front end must wire up. And the Payload admin panel - the React app your editors use every day - has its own accessibility considerations, including any custom fields or dashboard components you inject. This checklist focuses on the rendering and schema decisions that determine whether a Payload-powered site passes a WCAG 2.1 AA, EN 301 549, or EAA audit.
Common Accessibility Issues
Payload stores rich text as a Lexical (or legacy Slate) JSON tree, and you write a serializer that turns that tree into React or HTML. A naive serializer is the single most common accessibility failure in Payload projects: heading nodes rendered as styled paragraphs (or all as the same level), list nodes flattened into plain text, blockquotes and code blocks emitted as divs, and link nodes rendered without preserving their text. Screen reader users then get content with no headings to navigate, no list structure, and links that read as nothing.
Map every rich text node type to the correct semantic element in your serializer: heading nodes to h2-h6 (reserve h1 for the page title from another field), list nodes to ul/ol/li, quote nodes to blockquote, code nodes to pre/code, and link nodes to anchors with their text preserved and external/target safety handled. If you use the official @payloadcms/richtext-lexical React converter, extend its converters rather than rewriting from scratch. Test serialized output with a screen reader's headings and links lists.
// naive: everything becomes a styled <div>
function render(node) {
return <div className={node.type}>{node.text}</div>;
} // map node types to semantic elements
const Tag = { h2: 'h2', h3: 'h3', quote: 'blockquote' }[node.type] ?? 'p';
if (node.type === 'list') return <ul>{node.children.map(renderListItem)}</ul>;
if (node.type === 'link') return <a href={node.url}>{node.children.map(render)}</a>;
return <Tag>{node.children.map(render)}</Tag>; The Blocks field lets editors assemble a page from arbitrary block types, and each block maps to a React component you write. If those components choose heading levels by visual size, emit decorative wrappers as content, or stack multiple H1s when several blocks each render one, the page outline breaks. Because a block is reused across many pages, one badly-structured block component is a defect everywhere it appears.
Design block components to fit into a coherent document outline: render the page's single H1 from the page title field, and have content blocks use H2/H3 in context (consider passing a heading-level prop so a block adapts to where it sits). Use semantic section/article/figure elements rather than generic divs, and give each block component a Tab-only and screen reader review before it is reused. Treat the block component library as shared infrastructure that is audited once and trusted everywhere.
Payload's Upload-enabled collections store files but do not include an alt text field unless you add one, and even then it is optional unless you make it required. Teams frequently ship media collections with no alt field at all, so the front end has nothing to render, and every informative image reaches screen reader users as nothing or as a filename.
Add an alt text field to every Upload-enabled collection and make it required for collections that hold informative images (use a conditional or a validate function to allow an explicit 'decorative' choice that maps to alt=''). Render that alt value on every <img> in the front end. For media that is sometimes decorative and sometimes informative, store the alt with the usage (the block or field that references the image) so it matches context, and surface missing alt text to editors in the admin UI.
// media collection with no alt field; front end renders:
<img src={media.url} /> // collection fields: [{ name: 'alt', type: 'text', required: true }]
<img src={media.url} alt={media.alt} /> Navigation menus in Payload are commonly modeled as a relationship or array field (a Globals 'nav' config, for example) and rendered by the front end. A naive renderer outputs a list of links with no nav landmark, no skip link, and - for dropdown/mega menus - submenus that open only on hover and cannot be operated or dismissed with the keyboard. Keyboard and screen reader users then cannot reach or use the navigation reliably.
Render primary navigation inside a <nav aria-label> landmark, provide a skip-to-content link as the first focusable element, and build submenus as keyboard-operable disclosures (Enter/Space to open, Escape to close, focus management) with aria-expanded reflecting state. Keep the markup a real list of links. Because the data is structured, you can build one accessible nav component and drive it from the relationship/array data for the whole site.
Whether forms come from the official Form Builder plugin or a custom collection, the front end renders the fields, and a quick implementation tends to use placeholder text instead of labels and show submission errors in a banner that is not tied to the field that failed. Placeholders are not reliable labels and disappear on focus, and an unassociated error message cannot be navigated to or reliably announced.
Render a persistent visible <label> for every field, associated with for/id; do not use placeholders as labels. Mark required fields with required and aria-required, set aria-invalid on failed fields, and tie each error message to its field with aria-describedby. Put an error summary in an aria-live region or move focus to it on submit failure. When using the Form Builder plugin, render its field schema into this accessible markup rather than a minimal default.
Interactive blocks teams build for Payload front ends - accordions/FAQs, tabbed sections, carousels, modals, filter widgets - are frequently implemented as click-only div interactions with no keyboard operation, no visible focus, no managed focus for modals, and no ARIA state. Keyboard users cannot operate them and screen reader users get no information about state changes.
Implement interactive widgets against the WAI-ARIA Authoring Practices: real buttons for triggers, correct roles and aria-expanded/aria-selected/aria-controls state, keyboard interaction (arrow keys for tabs, Escape to close, focus trapping and return for modals), and a visible :focus-visible style. Prefer native elements (details/summary for simple disclosures, dialog for modals) where they fit. Build these as shared components and test each with the keyboard only before reuse.
Payload's admin panel is a React application your content editors use daily, and accessibility there matters for editors with disabilities. The core admin is reasonably usable, but custom field components, dashboard widgets, and UI you inject can introduce barriers - unlabeled custom inputs, click-only controls, color-only status, and lost focus - making the CMS itself hard to use with a keyboard or screen reader.
When building custom admin field components or views, use labeled form controls, real buttons, sufficient contrast, and keyboard operability just as you would on the front end. Reuse Payload's built-in field components where possible so you inherit their behavior. Do not convey status by color alone in dashboards. Test the editing workflow your team relies on with the keyboard and a screen reader so editors with disabilities can do their jobs.
Payload CMS-Specific Tips
- Payload is headless: it renders nothing public, so accessibility lives almost entirely in your front-end rendering layer. There is no theme silently handling landmarks, headings, or focus - if you do not build it, it is not there.
- The rich text serializer is the highest-leverage fix. Map every Lexical/Slate node type to the correct semantic element once, extend the official @payloadcms/richtext-lexical converters rather than reinventing them, and that fix applies to all content.
- Treat your Blocks field component library as shared infrastructure: audit each block component for heading level, alt text, and keyboard support once, and every page that uses it benefits.
- Add a required alt text field to every Upload-enabled collection (with an explicit decorative option) so the schema makes missing descriptions impossible rather than relying on editor discipline.
- Model navigation as structured data but render it through one accessible nav component - nav landmark, skip link, keyboard-operable submenus with aria-expanded - reused site-wide.
- Build interactive blocks (accordions, tabs, carousels, modals) against the WAI-ARIA Authoring Practices and prefer native details/summary and dialog where they fit; wire eslint-plugin-jsx-a11y and an automated axe check into CI to catch regressions.
- Don't forget the admin panel - it is a React app your editors use daily. Keep custom field components and dashboard widgets labeled, keyboard-operable, and not reliant on color alone.
Recommended Tools
eslint-plugin-jsx-a11y
An ESLint plugin that flags many accessibility issues in your React rendering layer (missing alt, invalid ARIA, non-interactive elements with handlers) at author time, before they ship.
@axe-core/react / axe DevTools
Deque's axe engine, usable as a browser extension on rendered pages and as @axe-core/react / @axe-core/playwright in automated tests, to catch WCAG issues in the front end Payload feeds.
Storybook a11y addon
Runs axe checks on individual components in isolation - ideal for auditing each Payload block and rich text rendering as a reusable component before it lands in a page.
Payload CMS: Where Accessibility Responsibility Lives
| Plugin / Tool | Layer | What Payload Provides | What You Must Build |
|---|---|---|---|
| Rich text 1.3.1 | Lexical/Slate JSON tree + official converters | A serializer mapping every node type to semantic HTML | |
| Blocks field 1.3.1 | Composable block schema | Block components with correct headings, alt, and semantics | |
| Media / uploads 1.1.1 | Upload collections (no alt field by default) | A required alt field and rendering it on every image | |
| Navigation 2.4.1 | Relationship/array data for menus | A nav landmark, skip link, and keyboard-operable submenus | |
| Admin panel 4.1.2 | A reasonably usable React admin | Accessible custom fields, views, and dashboard widgets |
Frequently Asked Questions
Is a Payload CMS site accessible by default?
Payload is headless, so it does not render your public site at all - your own front-end code does. That means there is no default accessibility to inherit and no theme quietly adding landmarks, headings, focus management, or labels. Everything that reaches the browser is something you wrote, so the accessibility of a Payload site is entirely a function of your rendering layer and your schema. The upside is full control with nothing forcing inaccessible markup on you; the responsibility is that nothing is accessible unless you build it that way.
How do I render Payload's Lexical rich text accessibly?
Rich text is stored as a Lexical (or, in older projects, Slate) JSON tree, and you convert it to React/HTML with a serializer - that serializer is where most rich text accessibility is won or lost. Map each node type to the correct semantic element: heading nodes to h2-h6 (keep h1 for the page title), list nodes to ul/ol/li, quote nodes to blockquote, code to pre/code, and link nodes to anchors with their text preserved. Use and extend the official @payloadcms/richtext-lexical React converters rather than writing a naive one that wraps everything in divs, and verify the output with a screen reader's headings and links lists.
How do I make sure images from Payload have alt text?
Add an alt text field to every Upload-enabled collection and make it required for collections that hold informative images, ideally with an explicit 'decorative' option that maps to alt='' so editors must make a deliberate choice. Then render that value on every in your front end. Because the same media item can be reused in different contexts, consider storing the alt alongside the usage (the block or field referencing it) so the description matches the context, and surface missing alt text in the admin UI so editors catch it before publishing.
Does the Payload admin panel need to be accessible?
Yes, if editors with disabilities use it - the admin panel is a React application your team works in daily, and under employment-accessibility expectations an internal tool should be usable too. Payload's core admin is reasonably usable, but custom field components, dashboard widgets, and any UI you inject can introduce barriers: unlabeled inputs, click-only controls, color-only status, lost focus. Build custom admin components with the same care as the front end - labeled controls, real buttons, sufficient contrast, keyboard operability - and reuse Payload's built-in field components where you can.
How do I test a Payload-powered site for accessibility?
Test at two layers. At the component layer, audit each block and the rich text renderer in isolation - the Storybook a11y addon and eslint-plugin-jsx-a11y catch many issues before they reach a page, and @axe-core/playwright can assert on them in CI. At the page layer, run axe DevTools on rendered routes and do a manual pass: keyboard-only navigation with visible focus, and a screen reader pass with NVDA + Firefox and VoiceOver + Safari. Because blocks and the serializer are reused everywhere, fixing them once at the component layer is the highest-leverage testing you can do.
Further Reading
- Ai Generated Code Accessibility Audit
- React Tutorial Accessibility Mistakes
- Aria Attributes Beginners Guide
- Automate Accessibility Fixes Github Action
- Webflow Cms Heading Hierarchy
- Accessible Forms Guide
Other CMS Checklists
- Strapi Accessibility Checklist
- Sanity Accessibility Checklist
- Contentful Accessibility Checklist
- Nextjs Accessibility Checklist
Get our free accessibility toolkit
We're building a simple accessibility checker for non-developers. Join the waitlist for early access and a free EAA compliance checklist.
No spam. Unsubscribe anytime.