An opinionated guide to Sanity Studio
Sanity Studio is an incredibly flexible tool with near limitless customisation. Here's how I use it.
Go to An opinionated guide to Sanity StudioObject 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 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.
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.
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.
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.
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.
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.
Sanity Studio is an incredibly flexible tool with near limitless customisation. Here's how I use it.
Go to An opinionated guide to Sanity StudioIt can be useful for testing plugins, front ends, or other integrations to have a Sanity Studio populated with fake content.
Go to How to generate massive amounts of demo content for SanitySetup "Live by Default" fetches and interactive live preview with Presentation in Sanity Studio
Go to Visual Editing with Next.js App Router and Sanity StudioSummarise form progression by decorating the entire editing form for a document with a component loaded at the root level.
Go to Create a document form progress component