
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:
| Slot | What it's for | Typical content |
|---|---|---|
title | The main line | The document's title or name |
subtitle | A second, smaller line | Author, date, status or category |
description | Longer text, shown in roomier layouts | An excerpt or summary |
media | The thumbnail | An 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:
bodyinselectpulls 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:
useFormValueand 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
mediais 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.


