Sanity logosanity.ioAll Systems Operational© Sanity 2026
Change Site Theme
Sanity logo

Documentation

    • Overview
    • Platform introduction
    • Next.js quickstart
    • Nuxt.js quickstart
    • Astro quickstart
    • React Router quickstart
    • Studio quickstart
    • Build with AI
    • Content Lake
    • Functions
    • APIs and SDKs
    • Visual Editing
    • Blueprints
    • Platform management
    • Dashboard
    • Studio
    • Canvas
    • Media Library
    • App SDK
    • Content Agent
    • HTTP API
    • CLI
    • Libraries
    • Specifications
    • Changelog
    • User guides
    • Developer guides
    • Courses and certifications
    • Join the community
    • Templates
Developer guides
Overview

  • Develop with AI

    Best practices

  • Query optimization

    Paginating with GROQ
    High performance GROQ

  • Roles and authentication

    Setting up Single Sign-On with SAML
    Third-Party Login (Deprecated)
    OAuth2
    Set up SSO authentication with SAML and Azure/Entra ID
    Set up SSO authentication with SAML and PingIdentity
    Set up SSO authentication with SAML and JumpCloud
    Reconcile users against internal systems
    Restrict Access to Specific Documents
    Setting up a Default Relay State for IdP Initiated - SAML Logins

  • Structured content

    Scalable navigation patterns
    An opinionated guide to Sanity Studio
    Browsing Content How You Want with Structure Builder
    Deciding on fields and relationships
    Create richer array item previews
    Dynamic folder structure using the currentUser and workflow states
    Create a time duration object field
    Level up Your Edit Modal with Next/Previous Navigation Buttons for Array Items
    Create a “coupon generator” string field input
    Managing redirects with Sanity
    Create a document form progress component
    Create an array input field with selectable templates
    Creating a Parent/Child Taxonomy
    Create interactive array items for featured elements
    Create a visual string selector field input
    Create a survey rating number field input
    How to use structured content for page building
    Create a recycling bin for deleted documents via Sanity Functions

  • Frontend integration

    Add live content to your application
    Forms with Sanity
    Vercel Integration
    Build your blog with Astro and Sanity
    How to implement front-end search with Sanity

  • Ecommerce

    Displaying Sanity content in Shopify
    Sanity Connect for Shopify
    Custom sync handlers for Sanity Connect

  • Integrating with other services

    A/B testing with Sanity and Growthbook
    Cookie consent integrations with Sanity
    Integrating external data sources with Sanity
    Klaviyo (email campaigns)
    Developing with Next.js on GitHub Codespaces

  • Adopting Sanity

    How to pitch Sanity.io to your team
    Convincing your clients to go with Sanity.io, rather than a traditional CMS
    Not-profit plan
    Agencies: Navigating the Spring 2025 Organization Changes
    How to generate massive amounts of demo content for Sanity
    How to implement Multi-tenancy with Sanity

  • GROQ

    GROQ-Powered Webhooks – Intro to Filters
    GROQ-Powered Webhooks – Intro to Projections

  • Portable Text

    Presenting Portable Text
    Add Inline blocks for the Portable Text Editor
    Beginners guide to Portable Text
    How to add custom YouTube blocks to Portable Text
    Converting Inline Styles to Sanity Block Decorators
    Add things to Portable Text
    Change the height of the PTE

  • Community and ecosystem

    Create your own Sanity template
    Community guides
    Community Code of Conduct
    Contribute to the ecosystem

  • Plugin development

    Migrating plugins to support Content Releases

On this page

Previous

Deciding on fields and relationships

Next

Dynamic folder structure using the currentUser and workflow states

Was this page helpful?

On this page

  • What you need to know:
  • Custom form components by example
  • What you’ll be making
  • Schema preparation
  • Create a custom preview component
  • Customize the component
  • Next steps
Developer guidesLast updated September 24, 2025

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

Loading...
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:

Loading...
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!

Loading...
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.

Loading...
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.
Array items with additional components
// ./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`,
      }
    },
  },
})
// ./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',
        }),
      ],
    }),
  ],
})
A plain array input with a configured 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>
  )
}
// ./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
  },
})
Array previews customised with the same Badge
// ./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>
  )
}
Each preview is now contextual to a value in the array item