Auto-generate alt text for Media Library assets

Official(made by Sanity team)

By Bram Doppen

Automatically generate accessible, multilingual alt text for images in your Sanity Media Library using Agent Actions and Sanity Functions

studio/aspects/altText.ts

import {defineAssetAspect, defineField, defineArrayMember} from 'sanity'

const languages = [
  {title: 'Dutch', value: 'nl'},
  {title: 'English', value: 'en'},
  {title: 'French', value: 'fr'},
  {title: 'German', value: 'de'},
]

export default defineAssetAspect({
  name: 'altText',
  title: 'Alternative text',
  description: 'Accessible alternative text for this asset, in one or more languages.',
  type: 'array',
  of: [
    defineArrayMember({
      name: 'altTextItem',
      type: 'object',
      fields: [
        defineField({
          name: 'language',
          type: 'string',
          description: 'The language that the alt text is written in',
          options: {
            list: languages,
            layout: 'radio',
          },
        }),
        defineField({
          name: 'value',
          title: 'Alternative text',
          type: 'string',
          description: 'Short description of the image, for screen readers (max ~100 characters).',
        }),
      ],
      preview: {
        select: {
          title: 'value',
          subtitle: 'language',
        },
      },
    }),
  ],
})

index.ts

import {documentEventHandler} from '@sanity/functions'
import {createClient} from '@sanity/client'

const MAX_KEYWORD_WAIT = 5 // How many times to retry (total attempts = MAX_KEYWORD_WAIT + 1)
const KEYWORD_WAIT_MS = 1500 // Wait 1.5 seconds between checks
const languages = ['nl', 'en', 'fr', 'de']

// We have to wait for the keywords to be available because they are auto-generated by the Media Library when an image is uploaded.
async function waitForKeywords(
  fetchKeywords: () => Promise<string[] | undefined>,
): Promise<string[] | undefined> {
  for (let i = 0; i <= MAX_KEYWORD_WAIT; i++) {
    console.log('Waiting for keywords...', i)
    const keywords = await fetchKeywords()
    if (keywords && keywords.length > 0) {
      return keywords
    }
    if (i < MAX_KEYWORD_WAIT) {
      await new Promise((res) => setTimeout(res, KEYWORD_WAIT_MS))
    }
  }
  return undefined
}

export const handler = documentEventHandler(async ({context, event}) => {
  const mlId = context.eventResourceId
  const {_id, currentVersion} = event.data
  const detailedAssetId = currentVersion?._ref

  if (!detailedAssetId) {
    console.log('No detailedAssetId found, skipping')
    return
  }

  // Create a Media Library client
  const mediaLibraryClient = createClient({
    token: context.clientOptions.token,
    useCdn: false,
    apiVersion: '2025-05-08',
    resource: {
      type: 'media-library',
      id: mlId,
    },
  })

  // Query keywords from the Media Library asset using client.fetch()
  const fetchKeywords = async () => {
    try {
      const result = await mediaLibraryClient.fetch<{keywords?: string[]}>(
        `*[_id == $assetId][0]{ "keywords": metadata.keywords }`,
        {assetId: detailedAssetId},
      )
      return result?.keywords || []
    } catch (err) {
      console.error('Failed fetching keywords from asset', err)
      return []
    }
  }

  const keywords = await waitForKeywords(fetchKeywords)

  if (!keywords || keywords.length === 0) {
    console.log('No keywords found after retries, skipping')
    return
  }

  // Generate alt text based on the keywords using Agent Actions
  const agentClient = createClient({
    ...context.clientOptions,
    dataset: 'production',
    apiVersion: 'vX',
  })

  // Generate alt text for each language separately for reliability
  const altTextItemsArray: {_key: string; _type: string; language: string; value: string}[] = []

  for (const lang of languages) {
    const altText = await agentClient.agent.action.prompt({
      instruction: `Given the following keywords: [${keywords.join(', ')}], generate a short (max 100 chars) alt text in language: ${lang}. Respond with just the alt text string, no quotes or formatting.`,
    })

    altTextItemsArray.push({
      _key: crypto.randomUUID(),
      _type: 'altTextItem',
      language: lang,
      value: String(altText).trim(),
    })
  }

  // Update the asset with the alt text using the Media Library client
  const result = await mediaLibraryClient
    .patch(_id)
    .setIfMissing({aspects: {}})
    .set({'aspects.altText': altTextItemsArray})
    .commit()

  console.log('Mutation response:', JSON.stringify(result, null, 2))
})

sanity.blueprint.ts

import {defineBlueprint, defineMediaLibraryAssetFunction} from '@sanity/blueprints'

export default defineBlueprint({
  resources: [
    defineMediaLibraryAssetFunction({
      name: 'media-library-auto-alt-text',
      memory: 2,
      timeout: 30,
      src: './functions/media-library-auto-alt-text',
      event: {
        on: ['create', 'update'],
        filter: 'assetType == "sanity.imageAsset" && !defined(aspects.altText)',
        projection: '{ _id, currentVersion }',
        resource: {
          type: 'media-library',
          id: '<your-media-library-id>', // TODO: replace with your media library id
        },
      },
    }),
  ],
})

The Problem: Content teams need to provide accessible alt text for images across multiple languages, but manually writing alt text for every asset is time-consuming and often inconsistent. As the asset count grows and audiences become more global, creating and maintaining high-quality multilingual alt text becomes a bottleneck, leading to accessibility gaps and a poorer experience for screen reader users.

The Solution: This function automatically generates concise, descriptive alt text for images in your Media Library using Sanity’s Agent Actions. When an asset is created or updated, the function waits for Sanity’s auto-generated image keywords, then produces multilingual alt text and stores it directly on the asset as an aspect. Editors get accessible, editable alt text by default without manual effort.

Quick Start

View the complete example and source code.

Initialize blueprints if you haven't already:
npx sanity blueprints init

Add the function to your project:
npx sanity blueprints add function --example media-library-auto-alt-text

Deploy to production:
npx sanity blueprints deploy

How It Works

When an image asset is created or updated in the Media Library, the function automatically:

  • Triggers on asset create and update events in the Media Library
  • Checks whether alt text already exists to prevent update loops
  • Waits for Sanity’s machine-generated image keywords to become available (with retry logic)
  • Uses Sanity’s AI-powered Agent Actions to generate concise alt text, one language at a time
  • Stores the generated multilingual alt text in the asset’s aspects, where it can be edited in the Media Library UI

Key Benefits

  • Accessibility by default—every image gets descriptive alt text automatically
  • Multilingual support—generate alt text in multiple languages at once
  • Customizable prompts to match brand voice and accessibility guidelines
  • Editable results that content teams can review and refine
  • Significant time savings by eliminating manual alt text creation

Technical Implementation

The function uses Sanity’s Media Library Asset Functions combined with AI Agent Actions. It reads machine-generated image keywords from the underlying image asset, generates alt text per language, and writes the results to a custom Media Library aspect attached to the asset container.

Perfect For

  • Teams managing large, shared Media Libraries
  • Organizations with multilingual or international audiences
  • Accessibility-focused content teams
  • Projects that want automated metadata without changing editorial workflows

The function is compatible with any Sanity project that has Media Library enabled and can be easily customized to add or remove languages, adjust prompts, or integrate review and approval workflows.

Contributor

Official Recipes by Sanity

First Published Timestamp Function

Featured contributionOfficial(made by Sanity team)

Automatically track when content was first published with a timestamp that sets once and never overwrites, providing reliable publication history for analytics and editorial workflows.

Knut Melvær
Go to First Published Timestamp Function

Automatically tag blog posts

Featured contributionOfficial(made by Sanity team)

AI-powered automatic tagging for Sanity blog posts that analyzes content to generate 3 relevant tags, maintaining consistency by reusing existing tags from your content library.

Go to Automatically tag blog posts