Type something to search...
How to Customize List Previews in Sanity Studio

How to Customize List Previews in Sanity Studio

To customise list previews in Sanity Studio, add a preview property to a schema type. Inside it, select maps the preview's slots (title, subtitle, description and media) to field paths, including dot paths that follow references such as author.name. For anything beyond a straight mapping, add a prepare function that receives the selected values and returns the final title, subtitle, description and media. The same preview is used in document lists, search results, reference pickers and array items, so one change improves the whole Studio.

Good previews are the difference between a list of identical "Untitled" rows and a list editors can scan in seconds. Without configuration, the Studio guesses, usually picking a field called title or name and the first image it finds. This guide covers the preview slots, select paths, prepare, references and arrays, formatting dates and option lists, icons and custom media, sort-aware previews, previews for objects and array members, list layouts and the mistakes that make previews slow or broken.

The Preview Slots

A preview has four slots. Which ones are visible depends on where the preview is rendered:

SlotWhat it's forTypical content
titleThe main lineThe document's title or name
subtitleA second, smaller lineAuthor, date, status or category
descriptionLonger text, shown in roomier layoutsAn excerpt or summary
mediaThe thumbnailAn image field, an icon or a small React element

Every slot is optional. If there's no title, the Studio shows "Untitled", which is a good sign your preview needs attention.

Mapping Fields With select

The simplest preview only uses select. Keys are slot names and values are field paths:

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

export const post = defineType({
  name: 'post',
  title: 'Post',
  type: 'document',
  icon: DocumentTextIcon,
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({ name: 'slug', type: 'slug', options: { source: 'title' } }),
    defineField({ name: 'excerpt', type: 'text', rows: 3 }),
    defineField({ name: 'mainImage', type: 'image', options: { hotspot: true } }),
    defineField({ name: 'author', type: 'reference', to: [{ type: 'author' }] }),
    defineField({ name: 'publishedAt', type: 'datetime' }),
  ],
  preview: {
    select: {
      title: 'title',
      subtitle: 'slug.current',
      description: 'excerpt',
      media: 'mainImage',
    },
  },
})

Paths use dots for nested fields (slug.current, seo.title). The type's icon is used as the thumbnail when media is empty, so setting an icon on every document type is a cheap improvement on its own.

When media points at an image field, the Studio renders the thumbnail using the image's crop and hotspot, as described in hotspot and crop.

Transforming Values With prepare

When you need to combine or format values, select them under your own key names and build the result in prepare:

preview: {
  select: {
    title: 'title',
    authorName: 'author.name',
    publishedAt: 'publishedAt',
    media: 'mainImage',
  },
  prepare({ title, authorName, publishedAt, media }) {
    const date = publishedAt
      ? new Date(publishedAt).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })
      : 'Not scheduled'

    return {
      title: title || 'Untitled post',
      subtitle: [authorName, date].filter(Boolean).join(' · '),
      media,
    }
  },
},

The keys in select are yours to name. Only the object returned from prepare has to use the slot names. Always return strings for title, subtitle and description. A number or Date may render oddly or fail type checks, so convert with String() or a date formatter.

Following References

Dot notation follows references automatically. author.name reads the name field of the referenced author document, and you can chain further, such as author.company.name. Use a dot, not the GROQ -> arrow, and don't try to write GROQ here: the Studio builds the query for you. The references guide has more examples.

Each hop costs an extra lookup for every row in the list, so follow references only for the fields you display.

Previewing Arrays

Selecting a whole array can pull a lot of data into every row. Select specific indices instead:

// schemaTypes/book.ts
preview: {
  select: {
    title: 'title',
    author0: 'authors.0.name',
    author1: 'authors.1.name',
    author2: 'authors.2.name',
    cover: 'cover',
  },
  prepare({ title, author0, author1, author2, cover }) {
    const names = [author0, author1].filter(Boolean)
    const more = author2 ? ' and others' : ''
    return {
      title,
      subtitle: names.length ? `by ${names.join(', ')}${more}` : 'No authors',
      media: cover,
    }
  },
},

Here authors is an array of references, and each indexed path is resolved to the referenced author's name. Selecting the third item only to check whether it exists is a cheap way to show "and others". For a count of items, select a small number of indices like this rather than the full array.

Option Lists and Labels

For a string field with options.list of { title, value } pairs, the Studio shows the human-readable title when you map it directly in select. As soon as you add prepare, you receive the stored value instead. Keep the list in a constant and look up the title yourself:

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

const CATEGORIES = [
  { title: 'Conference', value: 'conf' },
  { title: 'Meetup', value: 'meetup' },
  { title: 'Workshop', value: 'workshop' },
]

export const event = defineType({
  name: 'event',
  type: 'document',
  fields: [
    defineField({ name: 'name', type: 'string' }),
    defineField({ name: 'category', type: 'string', options: { list: CATEGORIES } }),
    defineField({ name: 'startsAt', type: 'datetime' }),
  ],
  preview: {
    select: { title: 'name', category: 'category', startsAt: 'startsAt' },
    prepare({ title, category, startsAt }) {
      const label = CATEGORIES.find((c) => c.value === category)?.title ?? 'Uncategorised'
      const year = startsAt ? new Date(startsAt).getFullYear() : ''
      return { title, subtitle: `${label} ${year}`.trim() }
    },
  },
})

Icons and Custom Media

media accepts more than an image. You can return an icon component, which is handy for documents without images or for showing state at a glance:

import { CheckmarkCircleIcon, ClockIcon, DocumentIcon } from '@sanity/icons'

preview: {
  select: { title: 'title', media: 'mainImage', publishedAt: 'publishedAt' },
  prepare({ title, media, publishedAt }) {
    const isFuture = publishedAt && new Date(publishedAt) > new Date()
    return {
      title,
      subtitle: isFuture ? 'Scheduled' : publishedAt ? 'Live' : 'Draft only',
      media: media ?? (isFuture ? ClockIcon : publishedAt ? CheckmarkCircleIcon : DocumentIcon),
    }
  },
},

You can also return a small React element, such as a coloured dot or a two-letter avatar. If you do, give the schema file a .tsx extension. Keep these components simple: prepare runs everywhere a preview appears, including global search, outside any open document. Hooks that depend on the document form, such as useFormValue, throw an error there. Select the values you need and pass them in as props instead.

For a status badge inside the open document, rather than in lists, see document badges and previews.

Sort-Aware Previews

If a type defines orderings, editors can sort lists by them from the list's menu. prepare receives a second argument, viewOptions, with the active ordering, so you can show the field the list is sorted by:

// schemaTypes/movie.ts
export const movie = defineType({
  name: 'movie',
  type: 'document',
  fields: [
    defineField({ name: 'title', type: 'string' }),
    defineField({ name: 'genre', type: 'string' }),
    defineField({ name: 'releaseDate', type: 'date' }),
  ],
  orderings: [
    {
      title: 'Release date, newest',
      name: 'releaseDateDesc',
      by: [{ field: 'releaseDate', direction: 'desc' }],
    },
  ],
  preview: {
    select: { title: 'title', genre: 'genre', releaseDate: 'releaseDate' },
    prepare({ title, genre, releaseDate }, viewOptions) {
      const byDate = viewOptions?.ordering?.by.some((item) => item.field === 'releaseDate')
      return { title, subtitle: byDate ? releaseDate : genre }
    },
  },
})

It's a small detail that makes sorted lists far easier to read.

Previews for Objects and Array Members

preview works on object types too, and that's where it often matters most. Arrays of objects show each item's preview, so without one an array of cta objects or page-builder sections becomes a list of identical rows:

// schemaTypes/objects/hero.ts
import { defineField, defineType } from 'sanity'
import { BlockContentIcon } from '@sanity/icons'

export const hero = defineType({
  name: 'hero',
  title: 'Hero',
  type: 'object',
  icon: BlockContentIcon,
  fields: [
    defineField({ name: 'heading', type: 'string' }),
    defineField({ name: 'tagline', type: 'string' }),
    defineField({ name: 'image', type: 'image' }),
  ],
  preview: {
    select: { heading: 'heading', tagline: 'tagline', media: 'image' },
    prepare({ heading, tagline, media }) {
      return {
        title: heading || 'Hero (no heading)',
        subtitle: tagline ? `Hero · ${tagline}` : 'Hero',
        media,
      }
    },
  },
})

Prefixing the subtitle with the block type ("Hero", "Gallery", "Quote") helps editors understand a page-builder array at a glance. The same applies to custom blocks inside Portable Text; see custom Portable Text blocks and working with arrays.

List Layouts

The Structure tool renders previews with a layout. The default shows title, subtitle and media on one row. A custom desk structure can change the layout for a list, for example to detail, which has room for the description slot:

// structure.ts
import type { StructureResolver } from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Content')
    .items([
      S.listItem()
        .title('Posts')
        .child(S.documentTypeList('post').title('Posts').defaultLayout('detail')),
      ...S.documentTypeListItems().filter((item) => item.getId() !== 'post'),
    ])

The general layouts are compact, default, media and detail. Fill in description if you use detail, otherwise the extra space is wasted.

The Form Title

The large title at the top of the document form uses the same preview logic, so improving the preview also improves the form header. If a type's preview title doesn't make sense there, such as for a settings document, you can turn the form title off with __experimental_formPreviewTitle: false on the type. The __experimental prefix means it may change, so use it sparingly.

Common Mistakes

  • Selecting whole arrays or Portable Text: body in select pulls the entire body into every row. Select specific indices or a short excerpt field.
  • Expecting option titles in prepare: you get the stored value. Look the title up from a shared constant.
  • Returning non-strings: convert numbers and dates before returning them.
  • Using form hooks in media components: useFormValue and similar hooks fail outside the document editor.
  • Forgetting objects: array items without a preview are the most common source of "Untitled" rows.
  • No icon on the type: an icon gives every document a sensible thumbnail even when media is empty.

FAQ: Sanity List Previews

It's used wherever the Studio shows a compact version of a document or object: document lists, global search results, reference pickers, array items and the title at the top of the document form.

No. preview.select takes field paths, not GROQ. Use dot notation, such as author.name, to follow references, and indices such as tags.0 for array items. The Studio builds the query itself.

When you add a prepare function, the Studio passes the stored value of a list option, not its display title. Keep the options in a constant and look up the matching title inside prepare.

Return an icon component from @sanity/icons as media in prepare, or set icon on the schema type so it's used whenever the media slot is empty.

Yes, through the components.preview option on a type, but custom preview components only render in lists inside the document pane, such as arrays, not in the top-level Structure tool document lists. For most needs, select and prepare are enough.

Yes. Document lists show the draft version if one exists, so edits to fields used in the preview appear in the list as editors type.


Conclusion

List previews are controlled by one property on each schema type. Use select to map fields to the title, subtitle, description and media slots, follow references and array items with dot paths, and add prepare when you need to format dates, look up option titles, show icons or react to the current sort order.

Go through your schema and give every document type an icon and a preview, then do the same for every object type used in arrays. It takes an afternoon, and it removes the "Untitled" rows and guesswork that slow editors down every day.

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