Create richer array item previews
Object types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.
This developer guide was contributed by Simeon Griggs (Principal Educator).
Object types use a preview property to display contextual information about an item when they are inside of an array; customizing the preview component can make them even more useful for content creators.
What you need to know:
This guide assumes that you know how to set up and configure a Sanity Studio and have basic knowledge about defining a schema with document and field types. Basic knowledge of React and TypeScript is also useful, although you should be able to copy-paste the example code to get a runnable result.
Custom form components by example
One of Sanity Studio’s most powerful features is custom drop-in replacements for form fields. This guide is one in a series of code examples.
You can get more familiar with the Form Components API in the documentation.
- Create a “coupon generator” string field input
- Create a visual string selector field input
- Create a survey rating number field input
- Create a time duration object field
- Create an array input field with selectable templates
- Create interactive array items for featured elements
- Create richer array item previews
- Create a document form progress component
What you’ll be making

Schema preparation
In this guide, you’ll create a document type named campaign which has an array of offer fields.
Each offer has a title, discount and an expiry date.
Create a new object type for the offer, taking note of the detailed preview configuration.
// ./schema/offer/offerType.ts
import {defineField, defineType} from 'sanity'
import {TagIcon} from '@sanity/icons'
export const offerType = defineType({
name: 'offer',
title: 'Offer',
type: 'object',
icon: TagIcon,
fields: [
defineField({
name: 'title',
type: 'string',
validation: (Rule) => Rule.required().min(0).max(100),
}),
defineField({
name: 'discount',
description: 'Discount percentage',
type: 'number',
validation: (Rule) => Rule.required().min(0).max(100),
}),
defineField({
name: 'validUntil',
type: 'date',
}),
],
preview: {
select: {
title: 'title',
discount: 'discount',
validUntil: 'validUntil',
},
prepare({title, discount, validUntil}) {
return {
title: title,
subtitle: !discount
? 'No discount'
: validUntil
? `${discount}% discount until ${validUntil}`
: `${discount}% discount`,
}
},
},
})Also add a new document type for the campaign:
// ./schema/campaign.ts
import {defineField, defineType} from 'sanity'
export const campaignType = defineType({
name: 'campaign',
title: 'Campaign',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'offers',
type: 'array',
of: [
defineField({
name: 'offer',
type: 'offer',
}),
],
}),
],
})Add both these files to your Studio and remember to import them to the schemas loaded in sanity.config.ts
Create a new campaign document, add some offers and your document should look something like this:

The list item previews here are useful, but because dates are hard to read, it’s not immediately clear which dates are expired, soon to expire or far into the future. You could write validation rules to give warnings or errors, but perhaps you don’t want the dates to block publishing.
With some quick edits to the array item preview, these can be much richer.
Create a custom preview component
The preview form component works a little differently from others in the customization API. You cannot add click handlers or any other interactivity because in most cases this component is rendered inside a button. It also does not have access to the value of the field but instead receives the values of the preview property in the schema type definition.
So any customizations will need to be visual, and any extra data required will be deliberately passed down in the schema type definition.
Create a new component file for your item preview:
// ./schema/offer/OfferPreview.tsx
import {Badge, Flex, Box} from '@sanity/ui'
import {PreviewProps} from 'sanity'
export function OfferPreview(props: PreviewProps) {
return (
<Flex align="center">
<Box flex={1}>{props.renderDefault(props)}</Box>
<Badge tone="positive">Hello!</Badge>
</Flex>
)
}Notice how you can use renderDefault(props) to output the out-of-the-box UI that is defined in the function for the prepare property.
And load it into the offer schema:
// ./schema/offer/offerType.ts
import {OfferPreview} from './OfferPreview'
export const offerType = defineType({
name: 'offer',
// ...other settings
components: {preview: OfferPreview},
preview: {
select: {
title: 'title',
discount: 'discount',
validUntil: 'validUntil',
},
// Remove "prepare" from the preview key!
// You'll handle this in the component soon
},
})Return to your documents and look at the offers array. There’s a little green badge alongside each one.
It’s pretty!

But pretty useless. Since the valid date is available to the component, you can update the component to display a different badge depending on the value of the date.
Customize the component
Update the custom preview component to use the code below.
Because this is TypeScript, you’ll notice the need to recast the props, this is because a component’s PreviewProps type does not receive the field’s value. So instead the the offer schema preview passed down discount and validUntil where usually you would setup title and subtitle.
The component intercepts these values, performs some logic to generate a new subtitle for the props.renderDefault(props) and also displays a relevant Badge alongside the preview.
// ./schema/offer/OfferPreview.tsx
import {useMemo, PropsWithChildren} from 'react'
import {Badge, Flex, Box, BadgeProps} from '@sanity/ui'
import {PreviewProps} from 'sanity'
type CastPreviewProps = PreviewProps & {
discount?: number
validUntil?: string
}
export function OfferPreview(props: PreviewProps) {
// Item previews don't have access to the field's value or path
// So we are passing in non-standard props in the schema
// And recasting the type here to match
const castProps = props as CastPreviewProps
const {discount, validUntil} = castProps
const badgeProps: (PropsWithChildren & BadgeProps) | null = useMemo(() => {
if (!validUntil) {
return null
}
const validUntilDate = new Date(validUntil)
if (validUntilDate < new Date()) {
// Offer has expired
return {
children: 'Expired',
tone: 'critical',
}
} else if (validUntilDate < new Date(Date.now() + 1000 * 60 * 60 * 24 * 7)) {
// Offer expires in less than a week
return {
children: 'Expiring soon',
tone: 'caution',
}
} else {
// Offer is still valid
return {
children: 'Valid',
tone: 'positive',
}
}
}, [validUntil])
const subtitle = !discount
? 'No discount'
: validUntil
? `${discount}% discount until ${validUntil}`
: `${discount}% discount`
return (
<Flex align="center">
{/* Customize the subtitle for the built-in preview */}
<Box flex={1}>{props.renderDefault({...props, subtitle})}</Box>
{/* Add our custom badge */}
{badgeProps?.children ? (
<Badge mode="outline" tone={badgeProps.tone}>
{badgeProps.children}
</Badge>
) : null}
</Flex>
)
}Return to your document and take a look at the new contextual badges. It’s now much clearer for authors to understand the status of each item.

Next steps
- Consider also adding validation to display warnings or errors on the object if you require the date value to prevent the document from being published.
- Decorating preview items is just the beginning! You could take a similar approach to render richer previews like images.
Deciding on fields and relationships
NextDynamic folder structure using the currentUser and workflow states
Was this page helpful?