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 StudioMake repetitive content creation tasks a breeze by supplying content creators with buttons to populate complex fields.
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.
An array field with additional buttons that can add multiple items to the field. The buttons let you add multiple array items based on an assumed value in the referenced documents. In the video example below, you can see that selecting “+ Sales” add items with references to people who belong to the Sales department, as defined in their person
document.
In this example, you’ll be working on a seminar
type document with a field named hosts
which is an array of references to people
documents.
For this imagined scenario, our content creators regularly create new seminar documents, but the same people tend to host the same one based on the topic. Instead of making authors search and add each person one-by-one – we can provide them with some buttons to instantly add all people of a specific type, which they can then edit.
To prepare, create some new schema type files, first a person
document type:
// ./schema/personType.ts
import {defineField, defineType} from 'sanity'
import {UserIcon} from '@sanity/icons'
export const DEPARTMENTS = [
{title: 'Engineering', value: 'engineering'},
{title: 'Sales', value: 'sales'},
{title: 'Marketing', value: 'marketing'},
]
export const personType = defineType({
name: 'person',
title: 'Person',
type: 'document',
icon: UserIcon,
fields: [
defineField({
name: 'name',
type: 'string',
}),
defineField({
name: 'department',
type: 'string',
options: {list: DEPARTMENTS},
}),
],
preview: {
select: {
name: 'name',
department: 'department',
},
prepare(selection) {
const {name, department} = selection
return {
title: name,
subtitle: DEPARTMENTS.find((item) => item.value === department)?.title,
}
},
},
})
Second, the hosts
array of references:
// ./schema/hosts/hostsType.ts
import {defineField, defineType} from 'sanity'
export const hostsType = defineType({
name: 'hosts',
title: 'Hosts',
type: 'array',
of: [
defineField({
name: 'host',
type: 'reference',
to: [{type: 'person'}],
}),
],
})
Lastly, the seminar
document:
// ./schema/seminarType.ts
import {defineField, defineType} from 'sanity'
export const seminarType = defineType({
name: 'seminar',
title: 'Seminar',
type: 'document',
fields: [
defineField({
name: 'title',
type: 'string',
}),
defineField({
name: 'hosts',
type: 'hosts',
}),
],
})
Create these schema type files and ensure they’re imported to your schema
in sanity.config.ts
.
Once complete, you should be able to compose a new seminar document that looks like this:
This works, but it’s time-consuming! Each person needs to be added individually. Looking up people by their department is time consuming. You can quickly customize this to make it better!
When we talk about “decorated” components, it still uses the same customization API as seen in other custom form component guides (links in the introduction). It simply means we are only adding extra detail or interactivity around the field and not directly changing how it works.
In the code example below, the default array input is rendered by the props.renderDefault(props)
callback. This is useful because the array input is such a complex piece of the Studio; it wouldn’t be pleasant to try and recreate it!
Also, decorated inputs can compose. You may have a plugin that also wraps your input to bring additional functionality. As often as you can render the default input and let the Studio resolve the component, the better.
Make a new component in your project:
// ./schema/hosts/HostsInput.tsx
import {Grid, Stack, Button} from '@sanity/ui'
import {AddIcon} from '@sanity/icons'
import {ArrayOfObjectsInputProps} from 'sanity'
import {DEPARTMENTS} from '../personType'
export function HostsInput(props: ArrayOfObjectsInputProps) {
return (
<Stack space={3}>
{props.renderDefault(props)}
<Grid columns={DEPARTMENTS.length} gap={1}>
{DEPARTMENTS.map((department) => (
<Button key={department.value} icon={AddIcon} text={department.title} mode="ghost" />
))}
</Grid>
</Stack>
)
}
Then update your hosts
field to use it
// ./schemas/hosts/hostsType.ts
import {HostsInput} from './HostsInput'
export const hostsType = defineType({
name: 'hosts',
// ...all other settings
components: {input: HostsInput},
})
Now create or edit a new seminar
document and you will see the decorated hosts
array with some extra buttons.
Extra buttons that right now don’t do anything.
Ideally, when clicked, a query is run to find every person matching that department and attaches them as a reference to the array.
You’ll need to perform a few actions when a button is pushed:
person
document that has the same department value as the one which was clicked_key
value, the correct _type
value and a reference to the published person document _id
as a _ref
insert
patches which will append each person to the end of the arrayThe code below performs all of that!
// ./schema/hosts/HostsInput.tsx
import {Grid, Stack, Button} from '@sanity/ui'
import {AddIcon} from '@sanity/icons'
import {randomKey} from '@sanity/util/content'
import {ArrayOfObjectsInputProps, Reference, insert, setIfMissing, useClient} from 'sanity'
import {useCallback} from 'react'
import {DEPARTMENTS} from '../person'
export function HostsInput(props: ArrayOfObjectsInputProps) {
const {onChange} = props
const client = useClient({apiVersion: `2023-04-01`})
// When a department button is clicked
const handleClick = useCallback(
async (event: React.MouseEvent<HTMLButtonElement>) => {
// Find the value of the button, the department name
const department = event.currentTarget.value
const query = `*[
_type == "person" &&
department == $department &&
!(_id in path("drafts.**")
]._id`
const peopleIds: string[] = (await client.fetch(query, {department})) ?? []
const peopleReferences: Reference[] = peopleIds.map((personId) => ({
_key: randomKey(12),
_type: `host`,
_ref: personId
}))
// Individually "insert" items to append to the end of the array
const peoplePatches = peopleReferences.map((personReference) =>
insert([personReference], 'after', [-1])
)
// Patch the document
onChange([setIfMissing([]), ...peoplePatches])
// To reset the array instead you'd do this:
// onChange(set(peopleReferences))
},
[onChange, client]
)
return (
<Stack space={3}>
{props.renderDefault(props)}
<Grid columns={DEPARTMENTS.length} gap={1}>
{DEPARTMENTS.map((department) => (
<Button
key={department.value}
value={department.value}
icon={AddIcon}
text={department.title}
mode="ghost"
onClick={handleClick}
/>
))}
</Grid>
</Stack>
)
}
With this setup, you should now be able to click one of the buttons and see it populated with matching people – if those documents exist!
For a truly polished experience, you might like to add loading or patching states or toast pop-ups for feedback. See the next steps section below.
Take this input to the next level by adding
useToast
hook from Sanity UI to notify instances where no people are found, or once a successful patch has been completed.useState
hook could disable the field and all buttons while the patch is happening to prevent multiple clicks and race conditions.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