Level up your edit modal with next/previous navigation buttons for array items

Streamline your array edit workflow by enabling smooth navigation through array items within the default edit modal. This guide walks you through building a clean, user-friendly solution for your Sanity Studio.

When working with arrays in Sanity Studio, editing individual items can get tedious, especially when you have to open and close the modal for each one.

What if you could streamline that process with next and previous buttons right inside the modal?

In this guide, you'll learn how to enhance the default edit modal with navigation controls that let you move smoothly between array items, without ever closing the modal. It's a small user experience improvement that makes a big difference in editor efficiency and workflow satisfaction.

Let’s dive in! 🚀

Before digging into the code, it is useful to understand how array item components work.

Every item has its (object) input passed down to its props as children (JSX Elements) which means we can access them in a custom item component and add our buttons to the object input component rendered in the modal.

This is how the solution will look at the end: with buttons to navigate through array items without the need to close the modal.

You can find the complete code in the Finished code section.

Prerequisites

  • A studio with @sanity/ui 4.0.0 or later and @sanity/icons 5.0.0 or later. sanity 6.9.2 and later depend on these versions.
  • Familiarity with custom form components. See Array item components in the Form components API reference.

Create a custom component for the array items

Let's start then!

Create a file called ArrayItemWithNavigator.tsx in your studio components folder.

In that file, add this bare-bones item component:

import { FunctionComponent } from 'react'
import {
  ItemProps,
} from 'sanity'

const ArrayItemWithNavigator: FunctionComponent<ItemProps> = (props) => {
  
  return props.renderDefault({
    ...props,
    // this is how we can extend the props which get rendered out in the item
  })
}

export default ArrayItemWithNavigator

As you can see, we can extend the props passed down to renderDefault in order to change individual props.

Next, we’ll retrieve the array value so we can access all its items and their corresponding paths. These will be used later to navigate between items within the modal.

import { FunctionComponent } from 'react'
import {
  ItemProps,
  ObjectItem,
  useFormValue,
} from 'sanity'

const ArrayItemWithNavigator: FunctionComponent<ItemProps> = (props) => {
  // * Get the path to the array (parent) for reading its value and focusing
  const arrayPath = props.path.slice(0, -1)
  // * Get the array value from the form
  const arrayValue = useFormValue(arrayPath) as Array<
    ObjectItem & { title: string }
  >

  /** Find the previous and next item in the array
   *
   * Returns the previous and next item in the array
   */
  const findPreviousAndNextArrayItems = () => {
    // * Get the current item key
    const currentItemKey = (props.value as ObjectItem)?._key

    const currentIndex = arrayValue.findIndex(
      (item) => item._key === currentItemKey,
    )
    // return both the previous and next item in the array, and if currentIndex is the first item, previous will be the last item and vice versa.
    return {
      previous:
        currentIndex === 0
          ? arrayValue[arrayValue.length - 1]
          : arrayValue[currentIndex - 1],
      next:
        currentIndex === arrayValue.length - 1
          ? arrayValue[0]
          : arrayValue[currentIndex + 1],
    }
  }

  return props.renderDefault({
    ...props,
    // this is how we can extend the props which get rendered out in the item
  })
}

export default ArrayItemWithNavigator

Create a custom Children component

Alright, with that out of the way, let’s add a Children component to ArrayItemWithNavigator.tsx. Since it’s only used internally, we can place it right above the ArrayItemWithNavigator. At the top of the file, import Flex and Stack from @sanity/ui, and add ObjectItemProps and Path to the sanity import.

const Children = ({
  children,
  navigation,
  arrayPath,
}: {
  children: ObjectItemProps['children']
  navigation: {
    previous: ObjectItem & { title: string }
    next: ObjectItem & { title: string }
  }
  arrayPath: Path
}) => {

  return(
    <Stack>
      <Flex justify="flex-end" gap={4} id="navigatorButtons">
        {/* our buttons will go here */}
      </Flex>
      {children}
    </Stack>
  )
}

Next, inside the Children component, define a navigation handler for the buttons. The handler takes the _key of the item to open, then opens and focuses that item's title field.

To open and focus that path, use onPathOpen and onFocus, which you get from the useDocumentPane hook. This hook is internal and can change without notice, so read the warning that follows this example before you use it. Import useDocumentPane from sanity/structure.

//* We use this INTERNAL hook to focus the next or previous item in the array
  // Since it is internal changes can be made to it without notice -> ADD CLEAR DEBUGGING INSTRUCTIONS FOR YOURSELF HERE!
  const { onFocus, onPathOpen } = useDocumentPane()

  /** will open any item in the parentArray and loop over it */
  const handleNavigation = (key: string) => {
    onPathOpen(arrayPath.concat({ _key: key }, 'title'))
    onFocus(arrayPath.concat({ _key: key }, 'title'))
  }

The handler opens and focuses each item's title field, and the button tooltips show that field's value. If your array items don't have a title field, replace 'title' and .title with a field they do have. If the component renders outside a document pane, useDocumentPane throws DocumentPane: missing context value.

Define the buttons

With that in place, we need to add our buttons to the Flex component. We will also add tooltips to the buttons because we want our editors to have more insights into where they are navigating. In those tooltips, we will display the title of the previous/next item. Add Box, Button, and Text to the @sanity/ui import. Import Tooltip from @sanity/ui/tooltip, ArrowUpIcon from @sanity/icons/ArrowUp, and ArrowDownIcon from @sanity/icons/ArrowDown.

const Children = ({
  children,
  navigation,
  arrayPath,
}: {
  children: ObjectItemProps['children']
  navigation: {
    previous: ObjectItem & { title: string }
    next: ObjectItem & { title: string }
  }
  arrayPath: Path
}) => {

  return(
    <Stack>
      <Flex justify="flex-end" gap={4} id="navigatorButtons">
        {/* PREVIOUS BUTTON */}
        <Tooltip
          portal
          padding={3}
          content={
            <Box>
              <Stack gap={3}>
                <Box>
                  <Text size={1}>Open item: </Text>
                </Box>
                <Box>
                  <Text size={1} style={{ fontStyle: 'italic' }}>
                    {navigation.previous.title}
                  </Text>
                </Box>
              </Stack>
            </Box>
          }
        >
          <Button
            id="previous-array-item-button"
            text={'Previous item'}
            icon={ArrowUpIcon}
            onClick={() => handleNavigation(navigation.previous?._key)}
            mode="ghost"
            fontSize={1}
            padding={2}
          />
        </Tooltip>
        {/* NEXT BUTTON */}
        <Tooltip
          portal
          padding={3}
          content={
            <Box>
              <Stack gap={3}>
                <Box>
                  <Text size={1}>Open item: </Text>
                </Box>
                <Box>
                  <Text size={1} style={{ fontStyle: 'italic' }}>
                    {navigation.next.title}
                  </Text>
                </Box>
              </Stack>
            </Box>
          }
        >
          <Button
            id="next-array-item-button"
            text={'Next item'}
            icon={ArrowDownIcon}
            onClick={() => handleNavigation(navigation.next?._key)}
            mode="ghost"
            fontSize={1}
            padding={2}
          />
        </Tooltip>
      </Flex>
      {children}
    </Stack>
  )
}

Extend props.children with the custom Children component

Now that we have the custom Children component we can use it to extend children in the props we pass down to renderDefault in the array item component:

return props.renderDefault({
    ...props,
    //* Because children holds the object input component for the modal, we can extend what is going to be rendered in the modal.
    children: (
      <Children
        children={props.children}
        navigation={findPreviousAndNextArrayItems()}
        arrayPath={arrayPath}
      />
    ),
  })

Add the ArrayItemWithNavigator item component to array members

We're almost finished! The only remaining step is to add the custom item component to the array members in your field schema. Add this field to the fields array of the document type that should have it. YOUR_DOCUMENT_TYPE stands for that type's schema file. The import path assumes ArrayItemWithNavigator.tsx is in a components folder next to schemaTypes, so adjust it to match your project:

import { defineArrayMember, defineField } from 'sanity'
import ArrayItemWithNavigator from '../components/ArrayItemWithNavigator'

// Add this field to the `fields` array of your document type
defineField({
  name: 'arrayNavigator',
  title: 'Array with navigator',
  type: 'array',
  of: [
    defineArrayMember({
      type: 'object',
      name: 'item',
      components: { item: ArrayItemWithNavigator },
      fields: [
        defineField({
          name: 'title',
          type: 'string',
          title: 'Title',
          validation: (Rule) => Rule.required(),
        }),
        defineField({
          name: 'description',
          type: 'text',
          title: 'Description',
        }),
      ],
    }),
  ],
})

Finished code

And we're done 🥳 you will now be able to navigate between item edit modals, without closing them.

The finished code imports Tooltip from @sanity/ui/tooltip and each icon from its own @sanity/icons subpath. These imports require @sanity/ui 4.0.0 or later and @sanity/icons 5.0.0 or later.

import { ArrowDownIcon } from '@sanity/icons/ArrowDown'
import { ArrowUpIcon } from '@sanity/icons/ArrowUp'
import { Box, Button, Flex, Stack, Text } from '@sanity/ui'
import { Tooltip } from '@sanity/ui/tooltip'
import { FunctionComponent } from 'react'
import {
  ItemProps,
  ObjectItem,
  ObjectItemProps,
  Path,
  useFormValue,
} from 'sanity'
import { useDocumentPane } from 'sanity/structure'

const Children = ({
  children,
  navigation,
  arrayPath,
}: {
  children: ObjectItemProps['children']
  navigation: {
    previous: ObjectItem & { title: string }
    next: ObjectItem & { title: string }
  }
  arrayPath: Path
}) => {
  //* We use this INTERNAL hook to focus the next or previous item in the array
  // Since it is internal changes can be made to it without notice -> ADD CLEAR DEBUGGING INSTRUCTIONS FOR YOURSELF HERE!
  const { onFocus, onPathOpen } = useDocumentPane()

  /** will open any item in the parentArray and loop over it */
  const handleNavigation = (key: string) => {
    onPathOpen(arrayPath.concat({ _key: key }, 'title'))
    onFocus(arrayPath.concat({ _key: key }, 'title'))
  }

  return (
    <Stack>
      <Flex justify="flex-end" gap={4} id="navigatorButtons">
        <Tooltip
          portal
          padding={3}
          content={
            <Box>
              <Stack gap={3}>
                <Box>
                  <Text size={1}>Open item: </Text>
                </Box>
                <Box>
                  <Text size={1} style={{ fontStyle: 'italic' }}>
                    {navigation.previous.title}
                  </Text>
                </Box>
              </Stack>
            </Box>
          }
        >
          <Button
            id="previous-array-item-button"
            text={'Previous item'}
            icon={ArrowUpIcon}
            onClick={() => handleNavigation(navigation.previous?._key)}
            mode="ghost"
            fontSize={1}
            padding={2}
          />
        </Tooltip>
        <Tooltip
          portal
          padding={3}
          content={
            <Box>
              <Stack gap={3}>
                <Box>
                  <Text size={1}>Open item: </Text>
                </Box>
                <Box>
                  <Text size={1} style={{ fontStyle: 'italic' }}>
                    {navigation.next.title}
                  </Text>
                </Box>
              </Stack>
            </Box>
          }
        >
          <Button
            id="next-array-item-button"
            text={'Next item'}
            icon={ArrowDownIcon}
            onClick={() => handleNavigation(navigation.next?._key)}
            mode="ghost"
            fontSize={1}
            padding={2}
          />
        </Tooltip>
      </Flex>
      {children}
    </Stack>
  )
}
const ArrayItemWithNavigator: FunctionComponent<ItemProps> = (props) => {
  // * Get the path to the array (parent) for reading its value and focusing
  const arrayPath = props.path.slice(0, -1)
  // * Get the array value from the form
  const arrayValue = useFormValue(arrayPath) as Array<
    ObjectItem & { title: string }
  >

  /** Find the previous and next item in the array
   *
   * Returns the previous and next item in the array
   */
  const findPreviousAndNextArrayItems = () => {
    // * Get the current item key
    const currentItemKey = (props.value as ObjectItem)?._key

    const currentIndex = arrayValue.findIndex(
      (item) => item._key === currentItemKey,
    )
    // return both the previous and next item in the array, and if currentIndex is the first item, previous will be the last item and vice versa.
    return {
      previous:
        currentIndex === 0
          ? arrayValue[arrayValue.length - 1]
          : arrayValue[currentIndex - 1],
      next:
        currentIndex === arrayValue.length - 1
          ? arrayValue[0]
          : arrayValue[currentIndex + 1],
    }
  }

  return props.renderDefault({
    ...props,
    //* Because children holds the object input component for the modal, we can extend what is going to be rendered in the modal.
    children: (
      <Children
        children={props.children}
        navigation={findPreviousAndNextArrayItems()}
        arrayPath={arrayPath}
      />
    ),
  })
}

export default ArrayItemWithNavigator

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme