Create an array input field with selectable templates
Add buttons that fill complex fields in one click, so content creators skip repetitive tasks.
This developer guide was contributed by Simeon Griggs (Principal Educator).
Add buttons that fill complex fields in one click, so content creators skip repetitive tasks.
What you need to know
This guide assumes that you know how to set up and configure 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 can copy and paste the example code to get a runnable result.
Subpath icon imports such as @sanity/icons/User require @sanity/icons 5.0.0 or later. The sanity package depends on that version from v6.4.0.
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 reference.
- 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 (Current page)
- Create interactive array items for featured elements
- Create richer array item previews
- Create a document form progress component
What you’ll be making
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 following video, you can see that selecting “+ Sales” adds items with references to people who belong to the Sales department, as defined in their person document.
Set up the schema types
In this example, you’ll be working on a seminar type document with a field named hosts, which is an array of references to person 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, you can provide them with some buttons to instantly add all people from a specific department, which they can then edit.
To prepare, create some new schema type files, first a person document type:
import {defineField, defineType} from 'sanity'
import {UserIcon} from '@sanity/icons/User'
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:
import {defineArrayMember, defineType} from 'sanity'
export const hostsType = defineType({
name: 'hosts',
title: 'Hosts',
type: 'array',
of: [
defineArrayMember({
name: 'host',
type: 'reference',
to: [{type: 'person'}],
}),
],
})Lastly, the seminar document:
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!
Create a decorated component
A “decorated” component uses the same customization API as the other guides in the “Custom form components by example” series. It only adds extra detail or interactivity around the field and doesn’t change how the field works.
In the following code example, 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 to recreate it!
Also, decorated inputs can compose. You may have a plugin that also wraps your input to bring additional functionality. The more often you render the default input and let the studio resolve the component, the better.
Make a new component in your project:
import {Grid, Stack, Button} from '@sanity/ui'
import {AddIcon} from '@sanity/icons/Add'
import {ArrayOfObjectsInputProps} from 'sanity'
import {DEPARTMENTS} from '../personType'
export function HostsInput(props: ArrayOfObjectsInputProps) {
return (
<Stack gap={3}>
{props.renderDefault(props)}
<Grid gridTemplateColumns={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:
import {defineArrayMember, defineType} from 'sanity'
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.
The extra buttons don’t do anything yet.
Ideally, clicking a button runs a query to find every person in that department and attaches them as references to the array.
Fetch and use content from other documents
You’ll need to perform a few actions when a button is clicked:
- Perform a query to find every
persondocument that has the same department value as the one which was clicked - Map over every person and create an array item with a unique
_keyvalue, the correct_typevalue, and a reference to the published person document_idas a_ref - Create an array of
insertpatches which will append each person to the end of the array - Set the initial empty array value on the field if it is missing
The following code performs all of these actions:
import {Grid, Stack, Button} from '@sanity/ui'
import {AddIcon} from '@sanity/icons/Add'
import {randomKey} from '@sanity/util/content'
import {ArrayOfObjectsInputProps, Reference, insert, setIfMissing, useClient} from 'sanity'
import {useCallback} from 'react'
import {DEPARTMENTS} from '../personType'
export function HostsInput(props: ArrayOfObjectsInputProps) {
const {onChange} = props
const client = useClient({apiVersion: `2025-02-19`})
// 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`
// The Studio client defaults to the raw perspective, which also returns
// draft and release version IDs. Query published documents only.
const peopleIds: string[] =
(await client.fetch(query, {department}, {perspective: 'published'})) ?? []
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 gap={3}>
{props.renderDefault(props)}
<Grid gridTemplateColumns={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 can click one of the buttons and see the array 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. For ideas, see “Next steps.”
Next steps
You can improve this input further in these ways:
- The
useToasthook from Sanity UI to notify instances where no people are found, or once a successful patch has been completed. - The
useStatehook could disable the field and all buttons while the patch is happening to prevent multiple clicks and race conditions. - Using the document store and a listening query, you could show a count of the number of people resolved by the query on the button itself before the button is clicked!
Was this page helpful?
Feedback submitted, thanks for helping us improve the docs. Have you tried these alternatives?