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 StudioSave time going in-and-out of modals by moving some light interactivity to array items.
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 will learn how to:
renderProps
and Sanity UIpath
argument to patch values in specific array itemsIn this example you’ll create readingList
documents that have an array of recommendations
. The array items include an object with a reference to a book
, and whether it is “featured” or not.
Create the following schema types in your Studio to get started.
First, a simple document type for a book:
// ./schema/bookType.ts
import {defineField, defineType} from 'sanity'
import {BookIcon} from '@sanity/icons'
export const bookType = defineType({
name: 'book',
title: 'Book',
type: 'document',
icon: BookIcon,
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'author',
description: 'This field should be a reference, but is a string in this demo for brevity',
type: 'string',
}),
defineField({
name: 'year',
type: 'number',
}),
],
preview: {
select: {
title: 'title',
author: 'author',
year: 'year',
},
prepare: ({title, author, year}) => ({
title,
subtitle: `${author} (${year})`,
}),
},
})
Next, a recommendation
object schema.
Note the comprehensive preview key setup so that list items are displayed with rich information about the object.
// ./schema/recommendation/recommendationType.ts
import {defineField, defineType} from 'sanity'
import {BookIcon} from '@sanity/icons'
export const recommendationType = defineType({
name: 'recommendation',
title: 'Recommendation',
type: 'object',
fields: [
defineField({
name: 'book',
type: 'reference',
to: [{type: 'book'}],
}),
defineField({
name: 'featured',
type: 'boolean',
initialValue: false,
}),
],
preview: {
select: {
title: 'book.title',
author: 'book.author',
year: 'book.year',
featured: 'featured',
},
prepare: ({title, author, year, featured}) => ({
title: [featured ? '⭐️ ' : '', `${title ?? `No book selected`}`].join(` `),
subtitle: author && year ? `${author} (${year})` : undefined,
media: BookIcon,
}),
},
})
Lastly, you’ll need a place to use these fields. Create a new document schema named readingList
// ./schema/readingListType.ts
import {Reference, defineField, defineType, isKeyedObject} from 'sanity'
type Recommendation = {
_key?: string
book?: Reference
featured?: boolean
}
export const readingListType = defineType({
name: 'readingList',
title: 'Reading list',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'recommendations',
type: 'array',
of: [{type: 'recommendation'}],
validation: (rule) =>
rule.custom((items?: Recommendation[]) => {
const featuredItems = items ? items.filter((item) => item.featured) : []
if (featuredItems.length > 1) {
return {
paths: featuredItems.filter(isKeyedObject).map((item) => [{_key: item._key}]),
message: 'Only one book can be featured',
}
}
return true
}),
}),
],
})
Take note of the validation rule above that will look through the list and check if there is more than one “featured” item. If so, an array of paths
is returned to mark each featured item as invalid. It’s important to give authors absolute clarity if something is invalid and what must be done to resolve it.
Create and publish some book documents and a “Reading list” document with some recommendations. It should look something like this:
This is a great start; rich list previews and clear validation warnings where necessary.
The authoring experience could still be better. Consider the many operations our content creators will take to remove all the “featured” values from each array item individually!
Instead, you could create a custom item to render a button to write changes to the field without having to enter any modals.
A schema field’s “item” is used when an object is displayed in an array – including inside a Portable Text field.
For more details, see the Form Components documentation.
To start, create a new item component which will render a Switch
input component from Sanity UI, along side each array item’s out-of-the-box preview:
// ./schema/recommendation/RecommendationItem.tsx
import {ObjectItemProps} from 'sanity'
import {Box, Flex, Switch} from '@sanity/ui'
import {Recommendation} from './recommendationType'
export function RecommendationItem(props: ObjectItemProps<Recommendation>) {
return (
<Flex gap={3} paddingRight={2} align="center">
<Box flex={1}>{props.renderDefault(props)}</Box>
<Switch checked={props?.value?.featured} />
</Flex>
)
}
A TypeScript thing to notice is that the ObjectItemProps
type is generic and can take in the Recommendation
type, this will be applied to value
of props
.
Import this component in the recommendation
schema field and add it to the components.item
property in the schema type definition:
// ./schema/recommendation/recommendationType.ts
import {RecommendationItem} from './RecommendationItem'
export const recommendationType = defineType({
name: 'recommendation',
// ...all other settings
components: {item: RecommendationItem},
})
Now when editing the same field, you get the same experience, but an additional toggle has been added to the right-hand side of the item.
You can click it, but it won’t do anything … yet!
Customizing the array item is typically used to add extra context. Since this example will write changes to the document, you’ll need to dig a bit deeper for some functions.
The example below uses a hook currently marked as internal: useDocumentPane
. There may be upcoming changes to the Studio that break its functionality. This guide will be updated when that happens.
Update the component code to match the example below.
// ./schema/recommendation/RecommendationItem.tsx
import {ObjectItemProps, PatchEvent, set, useFormValue} from 'sanity'
import {Box, Flex, Switch} from '@sanity/ui'
import {useDocumentPane} from 'sanity/desk'
import {useCallback} from 'react'
import {Recommendation} from './recommendationType'
export function RecommendationItem(props: ObjectItemProps<Recommendation>) {
const {value, path} = props
// Item props don't have `onChange`, but we can get it from useDocumentPane()
// This hook is currently marked internal – be aware that this can break in
// future Studio updates
const {onChange} = useDocumentPane()
// Get the parent array to check if any other items are featured
const parentPath = path.slice(0, -1)
const allItems = useFormValue(parentPath) as Recommendation[]
const handleClick = useCallback(() => {
const nextValue = value?.featured ? false : true
const clickedFeaturedPath = [...path, 'featured']
const otherFeaturedPaths = allItems.length
? allItems
?.filter((p) => p._key !== value?._key && p.featured)
.map((p) => [...parentPath, {_key: p._key}, 'featured'])
: []
// Because onChange came from useDocumentPane
// we need to wrap it in a PatchEvent
// and supply the path to the field
onChange(
PatchEvent.from([
// Update this field
set(nextValue, clickedFeaturedPath),
// Maybe update other fields
...otherFeaturedPaths.map((path) => set(false, path)),
])
)
}, [value?.featured, value?._key, path, allItems, onChange, parentPath])
return (
<Flex gap={3} paddingRight={2} align="center">
<Box flex={1}>{props.renderDefault(props)}</Box>
<Switch checked={value?.featured} onClick={handleClick} />
</Flex>
)
}
Note some of the hooks being used to power this component.
useDocumentPane
contains the root-level context for many of the functions passed down to individual inputs. Because a custom item does not currently receive onChange
– like a custom input – the document context is where you need to access ituseFormValue
is a way to retrieve values from the current document at a specified path. Since this custom component loads for each individual item in the array, this hook is required to get the outer “parent” value of every item in the array. This is how the component knows to remove featured
from other items, when adding it to this item.handleClick
function updates the featured
value of this item to either true or false – as well as setting other items false if they are true. Notice how each set()
function includes a path to each specific item.Now back to your custom item component; not only can you update the featured value from the array list itself – other featured items will be set to false. Not only is this experience faster, but it’s also better! It’s impossible to put any item into an invalid state using these new controls.
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