Structure Builder cheat sheet

The Structure Builder is a powerful (but complex) set of methods. There are a few commonly used methods which you are likely to need in your projects, and they are presented here in quick, copy-and-paste-friendly form.

Pro tip

In order to use these code examples, you will need to configure the structureTool plugin in your sanity.config.ts file like below:

// ./sanity.config.ts

import {defineConfig} from 'sanity'
import {structureTool} from 'sanity/structure'

import {structure} from './structure'

export default defineConfig({
  // ...all other settings
  plugins: [
    structureTool({ structure }),
    // ...all other plugins
  ],
})

All document schema types

Your imported structure configuration should have the following set up at a minimum: a list, with a title, and an array passed into items().

The following examples you will paste into this root-level items() method.

// ./structure/index.ts

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list().title('Base').items(
    S.documentTypeListItems() // <= example code goes here
  )
documentTypeListItems() renders a document list for every document schema type in your workspace

Filtered list of all document schema types

The documentTypeListItems() method from above will render a list for every document schema type that is registered in the Studio config. Used together with some clever filtering, this method alone will take you a long way in setting up your document type list to your preference.

In the example below, the siteSettings document schema type is filtered out, but all other document types would be listed. Then we insert a divider, and finally the siteSettings schema document list is manually inserted.

// ./structure/index.ts

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Base')
    .items([
      // list all document types except 'siteSettings'
      ...S.documentTypeListItems().filter(
        (item) => item.getId() !== 'siteSettings',
      ),
      S.divider(),
      // then add the 'siteSettings' type separately
      S.documentTypeListItem('siteSettings').title(
        'Site settings',
      ),
    ])
In this example "Site settings" is filtered out of the default list, and manually placed below a dividing line

All documents of a specific type

documentTypeListItem() is a “batteries included” method for showing a list of documents of a given type. Works great for showing complete lists of documents with a custom title. A common usage would be wanting to pluralize the type name in the title.

S.documentTypeListItem('lesson').title('Lessons')

This would be inserted into the items() method like this:

// ./structure/index.ts

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Base')
    .items([
      S.documentTypeListItem('lesson').title('Lessons')
    ])
documentTypeListItem returns an unfiltered list of all documents of this type

Show document counts

Chain showCount() onto a list item to show how many documents of that type exist, as a badge beside the item. Available from Studio v6.17.0.

S.documentTypeListItem('lesson').title('Lessons').showCount()

To show counts on every document type, map over documentTypeListItems():

// ./structure/index.ts

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Base')
    .items(S.documentTypeListItems().map((item) => item.showCount()))
Document count badges beside list items in the Structure tool

The count covers every document of the type in the selected perspective and variant, and updates as documents change. It only works on lists of a whole document type, so the filtered lists below don't show one. Studio logs a warning in development for any item where it withholds the count. For the full rules, see showCount() in the API reference.

A note on more complex examples

As these examples grow more complicated, you may wish to extract them into “helper functions” so they can be more easily reused.

Also, going forward, assume the example code is to be inserted into the array passed into the root-level items(), as we will exclude the boilerplate code for brevity.

// ./structure/index.ts

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S) =>
  S.list()
    .title('Base')
    .items([
      // ⬇ From now on, we will just show this bit
      S.documentTypeListItem('lesson').title('Lessons')
      // ⬆ Replace this with the example code
    ])

Filtered lists of documents

To show documents of a single type, with an additional GROQ filter applied, you will first need to create a listItem which has a documentList as its child. The document list must have an API version if it contains a filter.

These lists are “static” because the values being passed into the filter are known ahead of time.

S.listItem()
  .title(`English lessons`)
  .child(
    S.documentList()
      .apiVersion('2024-06-01')
      .title(`English lessons`)
      .schemaType('lesson')
      .filter('_type == "lesson" && language == "en"'),
  )
This filtered list only shows a subset of documents that satisfy the filter

You may choose to map over an array of items and use a params method to modify the results of each filtered list.

const languages = [
  {id: 'en', title: 'English'},
  {id: 'es', title: 'Spanish'},
]

...languages.map((language) =>
  S.listItem()
    .title(`${language.title} lessons`)
    .child(
      S.documentList()
        .apiVersion('2024-06-01')
        .title(`${language.title} lessons`)
        .schemaType('lesson')
        .filter('_type == "lesson" && language == $language')
        .params({language: language.id}),
    ),
)
Multiple, unique filtered lists have been created by mapping over an array

Dynamic filtered lists of documents

Say you have a document type post which has an array of references to the document type category.

In the example below are unfiltered document lists to show all documents of those types, and then a top-level list of all category documents, but instead of rendering those documents as a child element, the ID of each document is used to create a filtered list of every post type document that has a reference to that category.

S.documentTypeListItem('post').title('Posts'),
S.documentTypeListItem('category').title('Categories'),
S.listItem()
  .title('Posts By Category')
  .child(
    S.documentTypeList('category')
      .title('Posts by Category')
      .child((categoryId) =>
        S.documentList()
          .apiVersion('2024-06-01')
          .title('Posts')
          .filter('_type == "post" && $categoryId in categories[]._ref')
          .params({categoryId}),
      ),
  )
Here a list of category documents is used to create filtered lists of post documents that have that category

You may also want to check out this guide on parent child relationships for a more complex setup which includes initial value templates so that new documents created within these lists have filtered values preset.

Grouped and nested document lists

Some document types may not need to be accessed as often and so to reduce visual noise may be better grouped together into a single menu item.

S.listItem()
  .title('Website')
  .child(
    S.list()
      .title('Website')
      .items([
        S.documentTypeListItem('siteSettings').title('Site Settings'),
        S.documentTypeListItem('redirects').title('Redirects'),
        S.documentTypeListItem('labels').title('Labels'),
      ]),
  )
These three document lists have been placed within a parent list item

Singleton documents

Prerequisites

A singleton is a document with a fixed ID that can be created exactly once, typically used to store content like site settings, navigation, or footer text.

To register a singleton, add it to the document.singletons configuration registry.

import {defineConfig, defineSingleton} from 'sanity'

export default defineConfig({
  // ...
  document: {
    singletons: [
      // If your singleton uses the same schema type and
      // document id, you can use the string shorthand. This
      // expresses that the "siteSettings" schema type drives
      // a singleton with the document id "siteSettings".
      'siteSettings',

      // If you need greater control over the singleton's 
      // configuration, provide a configuration object. You
      // can use the `defineSingleton` helper to make sure
      // the object is typed correctly.
      defineSingleton({
        documentId: 'springCampaign',
        schemaType: 'campaign',
        title: 'Spring campaign',
        initialValue: {season: 'spring'},
      }),
    ],
  },
})

Studio handles the rest. It prevents the creation of non-singleton documents, adds the singleton to the default Structure Tool configuration, and adds a “singleton” document badge.

Studios using the default Structure Tool configuration needs no additional configuration at all. If you have a custom Structure Tool resolver, use one of the new Structure Builder methods to make the singleton accessible in Structure Tool:

  • S.document().singleton(<id>) can be rendered as a list item’s child.
  • S.listItem().singleton(<id>) creates a list item and a child.
  • S.list().singletons([<id1>, <id2>, ...]) creates a list of singletons.

These methods compose into different levels of structure, so you might find yourself reaching for a different method depending on what you want to achieve.

// A single list item.
S.listItem().singleton('siteSettings')

// Every registered singleton, alongside the default document type lists.
S.list()
  .id('__root__')
  .title('Content')
  .items(S.documentTypeListItems())
  .singletons()

// A given set of singletons in a list of their own.
S.list().id('singletons').title('Singletons').singletons(['siteSettings', 'springCampaign'])

// A singleton document pane.
S.listItem()
  .title('Site Settings')
  .id('siteSettings')
  .child(S.document().singleton('siteSettings'))

Singleton documents (prior to Studio version 6.17.0)

If you’re using a Studio version prior to 6.17.0, you can still configure a list item to represent a singleton by explicitly setting its id, setting its document id, and rendering a document editor as its child.

S.listItem()
  .id('siteSettings')
  .schemaType('siteSettings')
  .title('Site Settings')
  .child(
    S.editor()
      .id('siteSettings')
      .schemaType('siteSettings')
      .documentId('siteSettings')
  )
This list item does not render a document list, it shows a single document that contains the ID determined in the structure configuration

Custom structure by user role

The structure configuration contains a second parameter (context) which contains all sorts of valuable information about the current state of the Studio, including the logged-in user and their roles.

In this example, a different set of items is displayed to an Administrator than a user of any other roles.

import type {StructureResolver} from 'sanity/structure'

export const structure: StructureResolver = (S, context) =>
  S.list()
    .title('Base')
    .items(
      context.currentUser?.roles.find((role) => role.name === 'administrator')
        ? [
            S.documentTypeListItem('post').title('Posts'),
            S.documentTypeListItem('category').title('Categories'),
            S.divider(),
            S.documentTypeListItem('siteSettings').title('Site Settings'),
          ]
        : [
            S.documentTypeListItem('post').title('Posts'),
            S.documentTypeListItem('category').title('Categories'),
          ],
    )
If the logged-in user was not an Administrator, they would not be shown the divider or "Site Settings" list

Was this page helpful?

sanity.ioAll Systems Operational© Sanity 2026
Change site theme