Custom grid UI for multi-type reference array selection in Sanity Studio

3 replies
Last updated: Aug 25, 2021
I have a document where one field is an array of references. This field can reference many different types, and the list that pops up when you click “Add” in the studio is becoming long. Is there anyway to customize the selection of type of reference to add to a list? Ideally we’d display them in a grid with a visual representation of each one. If the component doesn’t support different selections, any pointers on extension points or other building blocks we could use to create something would be appreciated 🙏
AI Update

Yes, you can absolutely customize the "Add" button selection for an array of references in Sanity Studio! There are a couple of approaches depending on how much customization you want.

The Decorated Component Approach

The cleanest way to add a visual grid selector is to create a custom input component that wraps the default array input. This is called a "decorated" component because you're adding functionality around the existing input rather than replacing it entirely.

Here's how you'd implement it:

First, create a custom input component that renders the default array input plus your custom buttons:

// ./schema/MyArrayInput.tsx
import {Grid, Stack, Button} from '@sanity/ui'
import {AddIcon} from '@sanity/icons'
import {ArrayOfObjectsInputProps} from 'sanity'

export function MyArrayInput(props: ArrayOfObjectsInputProps) {
  return (
    <Stack space={3}>
      {props.renderDefault(props)}
      <Grid columns={3} gap={2}>
        <Button 
          icon={AddIcon} 
          text="Type A" 
          mode="ghost"
          onClick={() => {/* add Type A reference */}}
        />
        <Button 
          icon={AddIcon} 
          text="Type B" 
          mode="ghost"
          onClick={() => {/* add Type B reference */}}
        />
        {/* Add more buttons as needed */}
      </Grid>
    </Stack>
  )
}

Then configure your array field to use this component:

defineField({
  name: 'myReferences',
  type: 'array',
  of: [
    {type: 'reference', to: [{type: 'typeA'}, {type: 'typeB'}, {type: 'typeC'}]}
  ],
  components: {
    input: MyArrayInput
  }
})

Making the Buttons Functional

To actually add references when buttons are clicked, you'll need to query for the documents and patch the array. Here's a complete example based on the official guide:

import {useClient, insert, setIfMissing, Reference} from 'sanity'
import {randomKey} from '@sanity/util/content'
import {useCallback} from 'react'

export function MyArrayInput(props: ArrayOfObjectsInputProps) {
  const {onChange} = props
  const client = useClient({apiVersion: '2023-04-01'})

  const handleAddType = useCallback(async (typeName: string) => {
    // Query for documents of this type
    const query = `*[_type == $typeName && !(_id in path("drafts.**"))]._id`
    const docIds: string[] = await client.fetch(query, {typeName})
    
    // Create reference objects
    const references: Reference[] = docIds.map(id => ({
      _key: randomKey(12),
      _type: 'reference',
      _ref: id
    }))
    
    // Patch the document
    const patches = references.map(ref => insert([ref], 'after', [-1]))
    onChange([setIfMissing([]), ...patches])
  }, [onChange, client])

  return (
    <Stack space={3}>
      {props.renderDefault(props)}
      <Grid columns={3} gap={2}>
        <Button onClick={() => handleAddType('typeA')}>Type A</Button>
        <Button onClick={() => handleAddType('typeB')}>Type B</Button>
      </Grid>
    </Stack>
  )
}

Adding Visual Representations

To display icons or images for each type in your grid, you can customize the buttons further:

<Grid columns={3} gap={2}>
  {TYPES.map(type => (
    <Button 
      key={type.value}
      mode="ghost"
      onClick={() => handleAddType(type.value)}
    >
      <Stack space={2} align="center">
        {type.icon && <type.icon />}
        <Text>{type.title}</Text>
      </Stack>
    </Button>
  ))}
</Grid>

Alternative: Conditional Fields Workaround

As mentioned in this community answer, you could also use an object wrapper with conditional fields to achieve type selection, though this is more of a workaround and less elegant than the custom input approach.

Key Resources

The decorated component approach gives you complete flexibility while preserving all the built-in functionality of the array input (drag-and-drop reordering, validation, etc.). You can style it however you like using Sanity UI components!

Show original thread
3 replies
I was able to get something along these lines hacked together by using conditional fields (currently in beta ). However, I have to put all of the options inside of an object. For example, you have an array:
    {
      name: 'things',
      title: 'Things',
      type: 'array',
      of: [
        { type: 'referenceObject'}
      ]
    }
Then you create your referenceObject that conditionally shows a reference based off of the currently selected sibling string.

export default {
  name: 'referenceObject',
  title: 'Reference Object',
  type: 'object',
  fields: [
    {
      name: 'type',
      title: 'Type',
      type: 'string',
      options: {
        list: [
          { title: 'Movie', value: 'movie'},
          { title: 'Person', value: 'person'},
          { title: 'Screening', value: 'screening'}
        ]
      }
    },
    {
      name: 'movieReference',
      type: 'reference',
      to: [{ type: 'movie' }],
      hidden: ({ parent }) => parent?.type &&
        parent?.type == 'movie' ? false : true
    },
    {
      name: 'personReference',
      type: 'reference',
      to: [{ type: 'person'}],
      hidden: ({ parent }) => parent?.type &&
        parent?.type == 'person' ? false : true
    },
    {
      name: 'screeningReference',
      type: 'reference',
      to: [{ type: 'screening'}],
      hidden: ({ parent }) => parent?.type &&
        parent?.type == 'screening' ? false : true
    },
  ]
}
It could probably be polished up and made functional!
Interesting, thanks!
Interesting, thanks!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?