Type something to search...
How to Add Alt Text Requirements to Sanity Images

How to Add Alt Text Requirements to Sanity Images

To add alt text requirements to Sanity images, add a string field named alt to the image's fields array and give it a custom validation rule that returns an error when an image has been uploaded but the alt text is empty. Checking context.parent.asset inside the rule means editors aren't blocked when the image itself is empty, and adding a decorative boolean lets them mark purely visual images that should have an empty alt="". Define this once as a reusable image type and use it everywhere, including inside Portable Text, so every image in your Studio follows the same rule.

Alt text describes an image for people using screen readers, appears when images fail to load and helps search engines understand your content. Sanity doesn't add an alt field to images by default, so if you don't model one, editors have nowhere to write it, and your front end ends up with empty or generic values. This guide shows how to add the field, enforce it with validation, handle decorative images, add quality warnings, apply the rule to rich text and galleries, fall back to asset-level alt text, generate drafts with AI Assist and audit existing content.

Why Enforce Alt Text in the CMS

Front-end code can't write good alt text. Only the person who chose the image knows why it's there. A product photo might need "Blue running shoe, side view, with white sole", while the same photo on a sustainability page might need "Running shoe made from recycled ocean plastic".

Enforcing alt text in Sanity puts the requirement where the knowledge is:

  • Editors see it at the right moment: The field sits next to the image, and validation errors appear before publishing.
  • It's consistent: One schema rule applies to every image field that uses the type.
  • It's auditable: Because alt text is structured data, you can query for missing values across your whole dataset.

Add an Alt Field

Image fields accept a fields array for extra data. Add alt there:

// schemaTypes/post.ts
import { defineField, defineType } from 'sanity'

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({
      name: 'mainImage',
      title: 'Main image',
      type: 'image',
      options: { hotspot: true },
      fields: [
        defineField({
          name: 'alt',
          title: 'Alternative text',
          type: 'string',
          description:
            'Describe what the image shows and why it matters here. Read aloud by screen readers.',
        }),
      ],
    }),
  ],
})

The alt field belongs on the image field rather than the asset because alt text depends on context. The same asset can need different descriptions in different documents. For a refresher on image fields, see how to handle images in Sanity.

Make Alt Text Required

The obvious approach is rule.required() on the alt field. It works, but it has a flaw: it also fires when there's no image at all, so an editor who hasn't uploaded an optional image gets an error about alt text for an image that doesn't exist.

A custom rule that checks the parent image fixes that:

defineField({
  name: 'alt',
  title: 'Alternative text',
  type: 'string',
  validation: (rule) =>
    rule.custom((alt, context) => {
      const image = context.parent as { asset?: { _ref?: string } } | undefined

      // No image uploaded, so no alt text needed
      if (!image?.asset?._ref) return true

      if (!alt || alt.trim().length === 0) {
        return 'Alt text is required when an image is added.'
      }

      return true
    }),
})

In a nested field, context.parent is the object that contains the field, which here is the image. Custom rules also run when the value is undefined, which is why the function handles an empty alt itself. Errors block publishing, and the message appears on the alt field so editors know exactly what to fix. The validation rules guide covers rule.custom() and the context object in more depth.

If the image itself is mandatory, combine this with rule.required().assetRequired() on the image field.

Allow Decorative Images

Not every image needs a description. Background textures, dividers and purely visual flourishes should have an empty alt="" so screen readers skip them. Forcing editors to write something for these leads to unhelpful text such as "decorative image".

Add a boolean that marks an image as decorative, hide the alt field when it's on, and skip validation:

// schemaTypes/objects/accessibleImage.ts
import { defineField, defineType } from 'sanity'

type ImageParent = {
  asset?: { _ref?: string }
  decorative?: boolean
}

export const accessibleImage = defineType({
  name: 'accessibleImage',
  title: 'Image',
  type: 'image',
  options: { hotspot: true },
  fields: [
    defineField({
      name: 'decorative',
      title: 'Decorative image',
      type: 'boolean',
      description: 'Turn on if the image is purely visual and adds no information.',
      initialValue: false,
    }),
    defineField({
      name: 'alt',
      title: 'Alternative text',
      type: 'string',
      description: 'Describe what the image shows and why it matters here.',
      hidden: ({ parent }) => Boolean((parent as ImageParent | undefined)?.decorative),
      validation: (rule) =>
        rule.custom((alt, context) => {
          const image = context.parent as ImageParent | undefined
          if (!image?.asset?._ref || image.decorative) return true
          if (!alt?.trim()) return 'Add alt text, or mark the image as decorative.'
          return true
        }),
    }),
    defineField({
      name: 'caption',
      type: 'string',
    }),
  ],
})

Register accessibleImage in your schema types and use type: 'accessibleImage' wherever you need an image. This is the core of the approach: one type, one rule, applied consistently. Conditional fields like this are covered in show and hide inputs dynamically.

Use the decorative option sparingly. If an image is a link or the only content of a button, it's never decorative, because the alt text is the link's accessible name.

Add Quality Warnings

Required alt text stops empty values, but not poor ones. Add a second rule with .warning() to flag common problems without blocking publishing:

const BANNED_PREFIXES = ['image of', 'picture of', 'photo of', 'graphic of']

defineField({
  name: 'alt',
  title: 'Alternative text',
  type: 'string',
  hidden: ({ parent }) => Boolean(parent?.decorative),
  validation: (rule) => [
    // Error: required unless decorative
    rule.custom((alt, context) => {
      const image = context.parent as { asset?: { _ref?: string }; decorative?: boolean }
      if (!image?.asset?._ref || image.decorative) return true
      return alt?.trim() ? true : 'Add alt text, or mark the image as decorative.'
    }),

    // Warning: too long
    rule.max(150).warning('Keep alt text concise. Put longer detail in the caption.'),

    // Warning: redundant phrasing or file names
    rule
      .custom((alt) => {
        if (!alt) return true
        const lower = alt.toLowerCase().trim()
        if (BANNED_PREFIXES.some((p) => lower.startsWith(p))) {
          return 'Screen readers already announce images. Start with what the image shows.'
        }
        if (/\.(jpe?g|png|webp|gif|avif)$/i.test(lower)) {
          return 'This looks like a file name. Describe the image instead.'
        }
        return true
      })
      .warning(),
  ],
})

Returning an array of rules lets you mix severities. Errors block publishing, while warnings show a yellow marker that editors can choose to act on. The 150 character limit is a guideline rather than a standard; adjust it to suit your content.

Enforce Alt Text in Portable Text

Images inside rich text are easy to forget. If your block content uses a plain image member, it has no alt field and no validation. Use your reusable type instead:

// schemaTypes/blockContent.ts
import { defineArrayMember, defineType } from 'sanity'

export const blockContent = defineType({
  name: 'blockContent',
  title: 'Block content',
  type: 'array',
  of: [
    defineArrayMember({ type: 'block' }),
    defineArrayMember({ type: 'accessibleImage' }),
  ],
})

Validation on fields inside array members works the same way, and errors show both on the block and in the document's validation panel. When rendering, read alt and decorative in your Portable Text image component, as shown in rendering Portable Text in React.

Galleries and Arrays

For an array of images, such as a gallery, the same type handles each item:

defineField({
  name: 'gallery',
  type: 'array',
  of: [defineArrayMember({ type: 'accessibleImage' })],
  validation: (rule) => rule.min(1).max(12),
})

Each item gets its own alt field and its own validation, so editors can see exactly which image is missing a description.

Fall Back to Asset-Level Alt Text

Sanity's asset documents have their own altText field. Studio doesn't edit it by default, but tools such as sanity-plugin-media do, letting editors set alt text once on the asset. You can use it as a fallback in GROQ:

*[_type == "post" && slug.current == $slug][0]{
  title,
  mainImage{
    ...,
    "alt": coalesce(alt, asset->altText),
    decorative
  }
}

coalesce() returns the first value that isn't null, so the document's alt text wins when present. Be careful about relying only on the fallback, though. Asset-level alt text describes the image in general, not why it's used in a particular place. The plugin and how it stores data are covered in using Sanity media library plugins.

If you use the fallback, your validation rule can't see the asset's alt text without fetching it. A rule can use context.getClient() to look it up, but that adds a request for every validation run, so for most projects it's simpler to keep alt text on the image field.

Generate Drafts With AI Assist

Sanity's AI Assist plugin, @sanity/assist, can generate image descriptions. Point it at your alt field with the aiAssist.imageDescriptionField option on the image type:

defineType({
  name: 'accessibleImage',
  type: 'image',
  options: {
    hotspot: true,
    aiAssist: {
      imageDescriptionField: 'alt',
    },
  },
  fields: [
    defineField({ name: 'alt', title: 'Alternative text', type: 'string' }),
  ],
})

This adds a Generate image description action to the field, which by default runs again whenever the image changes. AI Assist must be installed and available on your plan, so check Sanity's documentation for current availability.

Treat generated text as a first draft. A model can describe what's in an image, but it can't know why you chose it. Keep your validation and warnings in place, and ask editors to review every generated description.

Render Alt Text Correctly

On the front end, respect the decorative flag and never fall back to the file name or title:

// src/components/content-image.tsx
import Image from 'next/image'
import { urlFor } from '@/sanity/lib/image'

type ContentImageProps = {
  image: {
    asset: { _ref: string }
    alt?: string
    decorative?: boolean
  }
  width: number
  height: number
}

export function ContentImage({ image, width, height }: ContentImageProps) {
  const alt = image.decorative ? '' : (image.alt ?? '')

  return (
    <Image
      src={urlFor(image).width(width).height(height).auto('format').url()}
      alt={alt}
      width={width}
      height={height}
    />
  )
}

An empty alt="" is correct for decorative images: screen readers skip it. A missing alt attribute is not, because some screen readers will read the file name instead. next/image requires the alt prop for this reason.

Audit Existing Content

Adding validation doesn't fix images published before the rule existed. Existing documents only show errors when someone opens them. Find them with GROQ in Vision:

*[
  _type == "post" &&
  defined(mainImage.asset) &&
  mainImage.decorative != true &&
  (!defined(mainImage.alt) || mainImage.alt == "")
]{
  _id,
  title,
  "image": mainImage.asset->url
}

For images inside Portable Text, check each block:

*[_type == "post" && count(body[_type == "accessibleImage" && decorative != true && !defined(alt)]) > 0]{
  _id,
  title,
  "missing": count(body[_type == "accessibleImage" && decorative != true && !defined(alt)])
}

Share the list with your editors, or run npx sanity documents validate to check every document against your current schema rules from the command line.


FAQ: Alt Text in Sanity

Not on the image field. You add an alt field yourself in the image's fields array. Asset documents do have an altText property, which plugins such as sanity-plugin-media can edit, but it's shared by every use of the asset.

It fires even when no image has been uploaded, which blocks editors on optional image fields. A custom rule that checks context.parent.asset only requires alt text when there's an actual image.

Add a decorative boolean to the image type, hide the alt field when it's true and skip validation. On the front end, render alt="" for decorative images so screen readers ignore them.

No. Validation runs when documents are edited or validated, so older content can still be missing alt text. Query for images without alt in Vision, or run sanity documents validate, then ask editors to fill the gaps.

Yes, with the AI Assist plugin and the imageDescriptionField option on your image type. Treat the result as a draft, because only the editor knows why an image was chosen and what it needs to convey in context.

There's no fixed limit, but short, specific descriptions work best for screen reader users. Many teams warn at around 125 to 150 characters and move extra detail into a caption or the surrounding text.


Conclusion

Requiring alt text in Sanity takes one reusable image type: an alt field with a custom rule that checks context.parent.asset, a decorative toggle that hides the field and skips validation, and a few warnings that catch phrases like "image of" and file names. Use that type for main images, galleries and Portable Text alike, and every image in your Studio follows the same standard.

Validation stops new gaps, but you still need to fix old ones, so audit existing content with GROQ and sanity documents validate. Add AI Assist for first drafts if it suits your team, render alt="" for decorative images, and you'll have accessible images built into your content model rather than bolted on afterwards.

Tags :
Share :

Related Posts

Building a Static Site With Astro and Sanity

Building a Static Site With Astro and Sanity

To build a static site with Astro and Sanity, keep Astro's default output: 'static', configure @sanity/astro with useCdn: false, and fetch

Dive Deeper
Common Sanity Mistakes and How to Fix Them

Common Sanity Mistakes and How to Fix Them

The most common Sanity mistakes are using the CDN where you need fresh data, fetching whole documents with ..., leaving slugs unvalidated, ship

Dive Deeper
How to Create a Custom Sanity Plugin

How to Create a Custom Sanity Plugin

To create a custom Sanity plugin, wrap a piece of Studio configuration in definePlugin from the sanity package and add it to the plugins ar

Dive Deeper