Oct 19, 2026
Failing an accessibility audit doesn’t just mean failing to check a WCAG checkbox. It also means legal exposure, potential litigation, and a product that excludes users, your competitors will happily serve.
Most teams treat accessibility as a frontend fix, but by the time the frontend renders your content, half the decisions are already locked in by the CMS. Your CMS sits between your content authors and your output layer, and the choices it makes about how fields are structured and how alt text is handled either give your frontend the raw material to comply with accessibility requirements or guarantee it can't.
Understanding where the CMS's responsibility ends, and the frontend's begins, is the only way to build a defensible, auditable accessibility posture.
In this article, we’ll look at what that looks like for Contentstack, including which Contentstack controls are responsible, what your frontend code is responsible for, and how to structure your content model so accessibility is considered from the beginning.
Key Takeaways
-
Accessibility failures in headless CMS implementations are usually due to content-model issues, not frontend issues.
-
Contentstack's built-in Description field returns as
file.descriptionin the Delivery API and maps directly to the HTML alt attribute. -
Heading hierarchy breaks when editors can apply any heading level to body copy, but restricting H1, H2, and H3 from Rich Text Editor fields fixes this at the model level.
-
Oshyn helps enterprise teams build Contentstack content models that meet WCAG 2.1 Level AA requirements, ensuring accessibility is built in from the beginning.
The WCAG Baseline
This post references WCAG 2.1 Level AA, the most common accessibility standard for public-facing websites, which covers perceivability, operability, understandability, and robustness.
The CMS vs. the Frontend: Who Is Responsible for What?

| CMS responsibility | Frontend responsibility |
|---|---|
| Providing an alt text field for every image | Rendering alt attribute on <img> tags |
| Structuring heading levels in content types | Mapping CMS heading fields to correct <h1>–<h6> tags |
| Providing descriptive link text fields | Not overriding link text with CSS content: hacks |
| Storing captions and transcripts for media | Rendering <figcaption>, <track>, or ARIA descriptions |
| Offering separate title and aria_label fields | Applying aria-label where the title differs from the accessible name |
Alt Text for Assets
WCAG 2.1 — SC 1.1.1 Non-text Content (Level A)
All non-text content presented to the user has a text alternative that serves the equivalent purpose. Purely decorative content is implemented so it can be ignored by assistive technology.
The problem with relying on file names
When an editor uploads an image to Contentstack and adds it to a File field, only the asset URL is stored. By default, there is no alt text.
The tempting shortcut is to fall back to the filename, which the Delivery API returns as file.filename. That fails SC 1.1.1 for a structural reason, not a cosmetic one: a text alternative must be authored by someone who understands what the image communicates in this context. A filename is a storage artifact. It was produced by a camera, an export preset, or a designer's save dialogue — never by someone describing the image's purpose to a user who cannot see it.
Anti-pattern: relying on asset filename as alt text
DSC00312-cropped-final-FINAL.jpg is not a text alternative. Neither is superman_logo_PNG8.png, even though it looks descriptive — it names the file, not what the image conveys. Note that file.filename and file.description are two distinct fields in the Delivery API response. Rendering the filename into alt is an active decision your frontend makes, not an innocent default. Never auto-generate alt from filename.
The right pattern: a dedicated alt text field
Contentstack assets have a built-in Description field. Fill it when you upload the image — it is returned as file.description in the Delivery API and maps directly to the HTML alt attribute. No extra field on the content type is needed:
| Field UID | Type | Notes |
|---|---|---|
| cover_image | File | The image asset. Fill Description in the asset editor for alt text. |
Rendering with alt text in React
function BlogHero({ post }) {
return (
<figure>
<img
src={post.cover_image.url}
alt={post.cover_image_alt || ''}
// Empty string = decorative image (screen reader skips it)
/>
{post.cover_image_alt && (
<figcaption> {post.cover_image_alt}</figcaption>
)}
</figure>
);
}
Empty alt for decorative images.
If an image is purely decorative (a background texture, a divider illustration), the alt text field should be left empty, and your frontend should render alt="" — not omit the attribute. Screen readers skip images with empty alt text; they announce images with no alt attribute.
What the fix looks like in practice
In the Contentstack training stack, the character content type stores a profile image in a file field. Filling the asset's Description with “Crest of the House of El, symbolizes hope” and binding it to alt is the entire fix — no schema change, no new field, one editor action.


Superman character page. The <img> element has an alt attribute — a screen reader announces the image's filename with no further context.
Fix step 1: add a description to the asset in Contentstack

Contentstack asset editor for superman_logo_PNG8.png. The Description field is filled with “Crest of the House of El, symbolizes hope.” This value is returned as file.description in the Delivery API response.
Fix step 2: map file.description to the alt attribute

After: image with meaningful alt text

Superman character page after the fix. DevTools confirms the <img> element now carries a descriptive alt attribute sourced directly from the Contentstack asset.
Asset Description vs. a custom alt text field
Contentstack assets include a built-in Description field, accessible as file.description. For most cases, this is sufficient — one place to write it, every context that uses the asset benefits. Add a separate profile_image_alt field on the content type only when the same asset needs different alt text depending on context (e.g., a logo that is decorative on the homepage but informative on the About page).
Structured Content and Heading Hierarchy
Why heading order matters
Screen reader users navigate pages using headings as landmarks. If your page goes H1 H3 H2, the structure is broken. Contentstack does not enforce heading order — that is a content modeling and editorial discipline problem.
Model heading fields explicitly
Avoid a single Rich Text field where editors can insert any heading they like. Instead, expose heading levels as explicit fields:
| Field UID | Type | Purpose |
|---|---|---|
| page_title | Single line text | Maps to <h1> — one per page |
| section_heading | Single line text | Maps to <h2> per section |
| subsection_heading | Single line text | Maps to <h3> |
| body | Rich Text | Prose only, restrict to <h4>–<h6> |
Restrict heading levels in the Rich Text Editor.
Contentstack's Rich Text Editor allows editors to insert headings by default. Configure the RTE toolbar to disable H1, H2, and H3 within body copy fields — those levels should only come from dedicated heading fields. This prevents the heading hierarchy from being broken when an editor formats a paragraph as an H1.
Link Text and Calls to Action
Anti-pattern: “Click here” and “Read more”
If your CTA content type has a field called button_text and editors fill it with “Click here”, every link on the page has the same accessible name. Screen reader users who navigate by links hear a list of identical “Click here” announcements.
The fix is a descriptive CTA model:
| Field UID | Type | Purpose |
|---|---|---|
| cta_label | Single line text | Visible button text: ``Start free trial'' |
| cta_url | Link | The destination |
| cta_aria_label | Single line text | Optional override for screen readers |
Rendering a CTA with an optional aria-label
<a
href={cta.cta_url}
aria-label={cta.cta_aria_label || undefined}
>
{cta.cta_label}
</a>
Media: Video and Audio
If your Contentstack stack stores video or audio assets, build your content type to support captions and transcripts:
| Field UID | Type | Purpose |
|---|---|---|
| video | File | The video asset (.mp4, .webm) |
| video_caption | File | .vtt caption file |
| transcript | Multi-line text | Plain text transcript |
| video_title | Single line text | Used as aria-label on the video element |
SEO Metadata and Open Graph (Bonus Accessibility)
SEO metadata is not purely an accessibility concern; well-structured metadata also helps screen readers and assistive tools that rely on page metadata.
Global Field: SEO
Define a Global Field called seo and include it in every content type. This ensures consistent metadata across the site without duplicating schema.
| Field UID | Type |
|---|---|
| seo.meta_title | Single line text (max 60 chars) |
| seo.meta_description | Multi-line text (max 160 chars) |
| seo.og_image | File |
| seo.og_image_alt | Single line text |
| seo.canonical_url | Single line text |
| seo.no_index | Boolean |
Editorial Guidelines You Should Publish Internally
Everything above is a modeling decision a developer makes once. The rules below are decisions an editor makes every day, and no content model can enforce them. This is where most accessibility regressions actually come from — not from a missing field, but from a correctly modeled field filled badly.
Publish this as a table in your team's style guide rather than a list of dos. The middle column is the part that makes it stick: editors follow rules they understand the consequences of, and ignore rules that read as arbitrary house style.
| Rule | What breaks without it | Criterion |
|---|---|---|
| Write CTA labels as actions: “Download the guide”, not “Click here” | A link list read by a screen reader becomes fourteen identical entries. The user must visit each one to find out where it goes. | 2.4.4 (A) |
| Keep the visible label inside the aria_label | A speech-input user says the words on the button, and nothing happens, because the accessible name does not contain them. | 2.5.3 (A) |
| Never skip heading levels; never pick a level for its size | Heading navigation — how most screen reader users skim a page — returns a broken outline. Visual hierarchy and programmatic hierarchy diverge. | 1.3.1 (A) |
| Captions must include non-speech audio: [door slams], [tense music] | A deaf viewer gets the dialogue but loses the tension, the off-screen event, and the speaker's identity. A speech-only .vtt does not satisfy the criterion. | 1.2.2 (A) |
| Never start alt text with “Image of” or “Photo of” | The screen reader has already announced the element as an image. The user hears “image, image of a woman at a laptop” on every asset. | 1.1.1 (A) |
| Never place meaningful text inside an image | The text cannot be resized, translated, recolored, or read aloud. This includes charts, quote cards, and infographics with baked-in labels. | 1.4.5 (AA) |
| Set the locale on every entry, and never mix languages in one field | The frontend emits no lang attribute, so a Spanish entry is read aloud with English phonetics — unintelligible rather than merely accented. | 3.1.1 / 3.1.2 (A) |
| Build tables with a real header row, not a bolded first row | Screen readers announce the column header before each cell. Without header markup, the user hears a stream of disconnected values. | 1.3.1 (A) |
| Write meta_title to identify the page out of context, not to repeat the <h1> | Twelve open tabs all announce the same title. The user cannot tell which one they wanted. | 2.4.2 (A) |
Make the rule visible where the decision is made
Contentstack lets you set help text and instructions on every field in the content type builder. A style guide nobody opens changes nothing; “Describe what the image conveys. Do not start with `Image of'. Leave empty if decorative.” sitting under the Description field changes behavior on the first upload. Move as many of these rules as you can out of the document and into the field definition.
Testing Accessibility
Contentstack itself is not the thing to test — your rendered frontend is. Run these tools on your published pages:
| Tool | What it checks |
|---|---|
| axe DevTools | Automated WCAG audit in browser DevTools |
| Lighthouse | Built into Chrome — includes accessibility score |
| NVDA / VoiceOver | Manual screen reader testing |
| Colour Contrast Analyser | Color contrast ratio for text and UI elements |
| WAVE | Visual accessibility report overlay |
Automated tools are a floor, not a ceiling
Auditing a modern web application with automated scanners alone is like critiquing a novel with a spellchecker. It will catch the misspellings, and it will tell you nothing about whether the book works.
Automated tools catch roughly 30–40% of WCAG issues. The remaining 60% can only be found by someone who understands how a person actually operates assistive technology. That is a skill, not a checkbox — and it is the single biggest gap on most teams shipping accessible software. If nobody on the project can drive a screen reader, your audit has a 60% blind spot that no budget for tooling will close.
What the scanner cannot determine
The limitation is structural, not a question of tool quality. A scanner reads the DOM at one point in time. It cannot form an intent, traverse an interface, or judge whether what it heard matched what was meant.
| What the scan sees | What only a human can determine | Criterion |
|---|---|---|
| An alt attribute is present, or empty | Whether the image is genuinely decorative. Scanners routinely flag alt="" as an error or warning because they cannot tell decorative styling from a missing description — and they can never tell whether a filled alt text is accurate for the image. | 1.1.1 (A) |
| An element has tabindex | Whether the physical, linear journey through the interface makes sense for a keyboard-only or switch-device user. | 2.1.1 (A) |
| The DOM order of focusable elements | Whether focus actually lands where the user expects after opening a modal, submitting a form, or closing a drawer. | 2.4.3 (A) |
| Nothing | Whether a focus trap exists. Scanners miss them because detecting one requires performing a multi-step keyboard interaction rather than reading a snapshot. | 2.1.2 (A) |
| Headings and landmarks exist | Whether the heading outline is actually useful for skimming, and whether the labels describe what follows them. | 2.4.6 (AA) |
| The final DOM after an update | Whether the user was told anything changed. In a React, Angular, or Next.js app, clicking “Filter results” can silently replace the content without any announcement or focus change. | 4.1.3 (AA) |
How experienced screen reader users actually navigate
This is the part most teams get wrong, and it changes what you should be testing.
Experienced screen reader users do not listen to a page linearly from the top. Doing that would make the web unusably slow. They skim — jumping by heading, cycling through landmarks, pulling up a list of every link on the page, moving between form fields. The page structure is the navigation.
That is why heading hierarchy and landmark structure are not cosmetic concerns, and why the content modeling decisions earlier in this post matter more than they first appear. An expert testing with NVDA or JAWS replicates those exact behaviors: pull the heading list, pull the links list, tab through the form. If your structure only makes sense read top to bottom, it fails the way the software is actually used.
Where the CMS fits into manual testing
A scanner can confirm that every image on the page has an alt attribute. It cannot confirm that the Description an editor wrote describes that image. Pull a handful of entries per release and have a human compare the asset to its Description field with a screen reader running. This is the only check that catches an alt text that is present, well-formed, and describes the wrong photograph.
Budget for the skill, not just the tool
axe and Lighthouse are free. The 60% they cannot see requires a person who knows how to drive a screen reader, navigate by landmark, and recognize a focus trap. Whether you hire that person, train someone, or contract an audit, treat it as a line item — otherwise the automated score becomes a false sense of compliance, which is a worse position than knowing you have not tested.
Building an Accessible Contentstack Implementation
Accessibility in a headless CMS like Contentstack starts with the content model. If you have the correct model, then the frontend will have what it needs. However, if you leave it to the frontend alone, then you can end up patching a structural problem with surface-level code.
Oshyn helps enterprises build and maintain inclusive digital experiences that meet WCAG standards, mitigate legal risk, and reach every user. As a Contentstack partner, we also help build Contentstack solutions that meet WCAG compliance requirements and hold up when audited.
Whether you’re assessing your current accessibility gaps or are starting a new implementation and want to get the structure right, reach out to the Oshyn team for assistance.