Create an array input field with selectable templates

Add buttons that fill complex fields in one click, so content creators skip repetitive tasks.

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.

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:

Default string and array inputs

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.

A decorated component with additional buttons below the default input

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 person document that has the same department value as the one which was clicked
  • Map over every person and create an array item with a unique _key value, the correct _type value, and a reference to the published person document _id as a _ref
  • Create an array of insert patches 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.”

Clicking one of the bottom buttons will populate the array with many items

Next steps

You can improve this input further in these ways:

  • The useToast hook from Sanity UI to notify instances where no people are found, or once a successful patch has been completed.
  • The useState hook 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?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme