Sanity is a popular headless CMS for marketing sites, product documentation, and editorial publications, and it gives developers far more control over accessibility than a traditional CMS — but only if the schema is modeled with accessibility in mind from day one. Because Sanity stores content in a structured document model and ships it to the front end as JSON (Portable Text), accessibility lives at two layers: the schema layer (where the editor experience and required fields are defined) and the rendering layer (where the front-end framework, usually Next.js or Remix, decides how to turn Portable Text into HTML). When teams forget to make alt text a required image field, every uploaded image risks shipping with no text alternative. When a developer renders Portable Text without mapping the right block styles to

,

, and
    , the entire content area renders as a flat sequence of
    s with no heading hierarchy. With the European Accessibility Act enforceable since 28 June 2025 against any business with EU customers and ADA demand letters increasingly reaching marketing-led sites, Sanity teams need a checklist that addresses both the schema and the renderer. This guide focuses on what an editorial-and-engineering team can change today: image schema definitions, Portable Text serializers, link annotations, the Studio editor experience for non-technical authors, and the front-end framework patterns that turn structured content into accessible HTML. Each issue maps to the specific WCAG 2.1 success criterion it addresses so you can document remediation in your accessibility statement.

    Common Accessibility Issues

    critical

    Image Schema Type Does Not Require Alt Text

    WCAG 1.1.1

    Sanity's default image schema does not include an alt-text field. Teams who add a custom 'alt' field often forget to mark it as required, so editors can publish documents with no alt text and the front end renders <img> elements with no alt attribute or with an empty one. Across a content library of hundreds of articles, this becomes a systemic WCAG 1.1.1 failure that is invisible until a screen-reader user encounters it.

    How to fix:

    Update the image schema in your Studio config to add an alt field with validation that marks it required. Mark decorative images with a separate boolean toggle so editors can intentionally omit alt text. In the Portable Text serializer, default the alt attribute to the field value and fall back to an empty string only when the decorative toggle is true.

    Before
    // schemas/blockContent.js
    export default defineType({
      name: 'blockContent',
      type: 'array',
      of: [
        {type: 'block'},
        {type: 'image'} // no alt-text field
      ]
    })
    After
    // schemas/blockContent.js
    export default defineType({
      name: 'blockContent',
      type: 'array',
      of: [
        {type: 'block'},
        {
          type: 'image',
          fields: [
            {
              name: 'alt',
              type: 'string',
              title: 'Alt text',
              validation: (Rule) => Rule.required().error('Alt text is required for accessibility'),
            },
            {name: 'decorative', type: 'boolean', title: 'Decorative image (no alt needed)'}
          ]
        }
      ]
    })
    critical

    Portable Text Renders Headings as Styled Divs

    WCAG 1.3.1

    Without a custom serializer, Portable Text often renders all block styles (h1, h2, h3, normal) as <div> elements with class names instead of semantic <h1>, <h2>, <h3>, and <p>. Screen-reader users lose the ability to navigate by heading, and the page violates WCAG 1.3.1 because the visual hierarchy is no longer programmatically determinable.

    How to fix:

    Provide an explicit components/types/marks map to your Portable Text renderer so each block style produces the correct semantic tag. In Next.js with @portabletext/react, define a components prop that maps h1 to <h1>, h2 to <h2>, blockquote to <blockquote>, bullet to <ul>, and number to <ol>. Confirm that exactly one <h1> is used per page (typically the document title rendered outside Portable Text) so editors stay within heading-level conventions.

    Before
    // No components prop, default rendering
    <PortableText value={post.body} />
    After
    // next-app/[slug]/page.tsx
    import {PortableText, PortableTextComponents} from '@portabletext/react'
    
    const components: PortableTextComponents = {
      block: {
        h1: ({children}) => <h1>{children}</h1>,
        h2: ({children}) => <h2>{children}</h2>,
        h3: ({children}) => <h3>{children}</h3>,
        blockquote: ({children}) => <blockquote>{children}</blockquote>,
        normal: ({children}) => <p>{children}</p>,
      },
      list: {
        bullet: ({children}) => <ul>{children}</ul>,
        number: ({children}) => <ol>{children}</ol>,
      },
    }
    
    <PortableText value={post.body} components={components} />
    serious

    Link Annotations Lose External-Link Indicators

    WCAG 2.4.4

    Sanity's default link annotation stores only the href and stores no information about whether the link is internal or external, opens in a new tab, or links to a download. When rendered, all links look identical, which means users with cognitive disabilities cannot anticipate that a click will leave the site, and screen-reader users do not hear an announcement before being taken to a new tab.

    How to fix:

    Extend the link annotation schema with a checkbox for 'Opens in new tab' and a string field for an optional accessible name. In the renderer, append target="_blank" rel="noopener noreferrer" only when the editor has selected that option, and append a visually hidden ' (opens in new tab)' to the link text or use aria-label. Style external-link icons with role="presentation" so they do not double-announce.

    Before
    // Default annotation
    <a href={value.href}>{children}</a>
    After
    marks: {
      link: ({value, children}) => {
        const target = value.openInNewTab ? '_blank' : undefined
        return (
          <a
            href={value.href}
            target={target}
            rel={target === '_blank' ? 'noopener noreferrer' : undefined}
          >
            {children}
            {target === '_blank' && <span className="sr-only"> (opens in new tab)</span>}
          </a>
        )
      },
    }
    serious

    Studio Editor Lets Authors Skip Heading Levels

    WCAG 1.3.1

    Sanity Studio's block-content toolbar exposes h1 through h6 by default, allowing editors to drop in any heading level without enforcement of hierarchy. Editors typically choose levels by visual weight, producing pages that jump from h1 to h4 with no h2 or h3 between, breaking screen-reader navigation and violating WCAG 1.3.1.

    How to fix:

    In the block-content schema, restrict the styles array to h2, h3, and h4 (assuming the page template provides h1 from the title field). Add documentation in the editor onboarding that explains why h2 always comes first under the page title. Optionally add a custom validation rule that flags documents with heading-level skips before publish.

    Before
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'H1', value: 'h1'},
        {title: 'H2', value: 'h2'},
        {title: 'H3', value: 'h3'},
        {title: 'H4', value: 'h4'},
        {title: 'H5', value: 'h5'},
        {title: 'H6', value: 'h6'}
      ]
    }
    After
    {
      type: 'block',
      styles: [
        {title: 'Normal', value: 'normal'},
        {title: 'Heading 2', value: 'h2'},
        {title: 'Heading 3', value: 'h3'},
        {title: 'Heading 4', value: 'h4'},
        {title: 'Quote', value: 'blockquote'}
      ]
    }
    serious

    Image Field Stores Hotspot Crops Without Aspect-Ratio Awareness

    WCAG 1.4.10

    Sanity's hotspot tool lets editors crop and reposition images, and the front end commonly uses Sanity's image-url builder to request derivatives. Without setting explicit width and height attributes, images render with no aspect ratio, causing layout shift on slow networks and content-reflow problems for users at 400 percent zoom (WCAG 1.4.10 Reflow).

    How to fix:

    When generating image URLs, request width and height that match the rendered size and set the corresponding HTML attributes so the browser reserves space. For responsive layouts, use the imageDimensions metadata on the asset reference to compute aspect ratios and pass them to <Image> in Next.js. Always include alt text alongside dimensions to keep both reflow and screen-reader requirements satisfied.

    serious

    Reference Fields to Other Documents Render as Bare URLs

    WCAG 2.4.4

    When editors insert internal references (links to another Sanity document), the default renderer often outputs the document slug or ID as the visible link text instead of the document title. Visitors and screen-reader users encounter a list of meaningless URL strings that violate WCAG 2.4.4 (Link Purpose, In Context).

    How to fix:

    In your GROQ query, expand reference fields to include the linked document's title and slug. In the Portable Text renderer, use the resolved title as the link text and the slug as the href. If a reference is rendered without a custom child node (a 'bare' reference), apply a default that renders the title rather than the raw slug.

    Before
    // GROQ query
    *[_type == 'post'] {body}
    
    // Renders bare slug as link text
    <a href="/{ref.slug}">{ref.slug}</a>
    After
    // GROQ query
    *[_type == 'post'] {
      body[]{
        ...,
        markDefs[]{
          ...,
          _type == 'internalLink' => {
            'slug': @.reference->slug.current,
            'title': @.reference->title
          }
        }
      }
    }
    
    <a href="/{ref.slug}">{ref.title}</a>
    serious

    Front-End Framework Skips ARIA Landmarks

    WCAG 1.3.1

    Sanity is paired most often with Next.js, Remix, or Astro, and many starter templates wrap the Portable Text output in generic <div> elements without <main>, <header>, or <nav> landmarks. Screen-reader users lose the ability to jump between landmarks, and the page violates WCAG 1.3.1 because regions are not programmatically determinable.

    How to fix:

    Wrap the page layout in <header>, <nav>, <main>, and <footer> landmarks at the layout level (e.g. app/layout.tsx in Next.js). Add aria-labelledby to the <main> element pointing to the page title <h1>. If your design uses a side-rail of related content, mark it as <aside>. Verify with the Accessibility Insights extension that the page exposes exactly one <main>.

    Sanity-Specific Tips

    • Treat the schema as an accessibility contract. Required alt-text fields, restricted heading levels, and link annotations with new-tab metadata cost nothing to add at schema-creation time and are painful to retrofit later.
    • Run @sanity/dashboard with an accessibility-checker tile so editors see a per-document score in Studio before they publish.
    • Use Vercel preview deploys (or Netlify equivalent) to run axe-core or pa11y against the rendered front end on every pull request that changes either the schema or the front-end serializer.
    • When you change the Portable Text components map, audit a sample of existing documents because the JSON content stays the same but the rendered HTML may shift. Heading-level changes are especially easy to break.
    • Keep editor onboarding documentation in the Studio itself using @sanity/desk-tool's intent helpers, so new editors learn accessibility expectations the moment they open a new article.

    axe DevTools

    Browser extension that scans the rendered front-end of Sanity-powered sites for WCAG violations such as missing alt text, low contrast, and missing landmarks.

    @axe-core/react

    Library that runs axe automatically in your Next.js development build and logs WCAG issues to the console as you build pages from Sanity content.

    Sanity Studio Preview Pane

    Built-in preview pane that lets editors view the rendered article alongside the document, making it easier to catch alt-text gaps and heading skips before publish.

    Further Reading

    Other CMS Checklists