👀 Our most exciting product launch yet 🚀 Join us May 8th for Sanity Connect
Last updated April 22, 2023

Create richer array item previews

By Simeon Griggs

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.

What you’ll be making

Array items with additional components

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:

A plain array input with a configured preview

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!

Array previews customised with the same Badge

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.

Each preview is now contextual to a value in the array 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.

Sanity – build remarkable experiences at scale

Sanity Composable Content Cloud is the headless CMS that gives you (and your team) a content backend to drive websites and applications with modern tooling. It offers a real-time editing environment for content creators that’s easy to configure but designed to be customized with JavaScript and React when needed. With the hosted document store, you query content freely and easily integrate with any framework or data source to distribute and enrich content.

Sanity scales from weekend projects to enterprise needs and is used by companies like Puma, AT&T, Burger King, Tata, and Figma.

Other guides by author