> For AI agents: the complete Sanity documentation index is available at [https://www.sanity.io/docs/llms.txt](https://www.sanity.io/docs/llms.txt).

# Dynamically group list items with a GROQ filter

In this article, we'll use the documentList() method to dynamically group documents with a GROQ filter.

![A screenshot illustrating a "Filtered Posts" list that allows an editor to filter by category or author](https://cdn.sanity.io/images/3do82whm/next/4663eac51572134b8723b4ebae92dc31e8ec853b-1854x1010.png)

It's often useful to group documents automatically by some field's value or a combination of field values. Common examples are grouping documents by author, publishing date periods, editorial status, category, or even the dominant background color in a document’s main image. In this article, we'll create lists of filtered blog posts to allow for quicker discovery and editing.

If you're unfamiliar with setting up the Structure Builder API, be sure to check out the previous articles in this series.

#### Structure builder series

[1. Introduction and concepts](https://www.sanity.io/docs/studio/structure-builder-introduction)
This is an introduction to important concepts for the Structure Builder API.

[2. Set up Structure Builder in your project](https://www.sanity.io/docs/studio/set-up-structure-builder-to-override-the-default-list-view)
In this article, we'll explore how to initialize Structure Builder and override the default title of the "Content" list.

[3. Create a link to a single edit page in your main document type list](https://www.sanity.io/docs/studio/create-a-link-to-a-single-edit-page-in-your-main-document-type-list)
In this article, we'll explore how to add a link to a single document from the main document type list.

[4. Manually group items in your document list](https://www.sanity.io/docs/studio/manually-group-items-in-a-pane)
In this article, we'll manually group a few singleton "site setting" documents. 

[5. Dynamically group documents](https://www.sanity.io/docs/studio/dynamically-group-list-items-with-a-groq-filter)
(You're here)

[6. Create a custom document pane](https://www.sanity.io/docs/studio/create-custom-document-views-with-structure-builder)
In this article, we'll look at adding a custom document view to view the JSON data for our posts.

## Setting up the schema

In this article, we'll need some basic schema for a blog. For the sake of simplicity, we'll use the default schema that comes from creating a new Sanity project from the Sanity CLI.

To get the schema, run `npx sanity init` and create a new project. When prompted, select `yes` to `Use default dataset configuration?` and `Blog` from the `Select project template` options.

This will give you a project structure that contains the schema for `post`, which contains references for `author` and `category` schema. Combining this with the singletons made in the previous articles, we should have a desk structure that looks like this:

![A list of menu items under "Base": Settings, Post, Author, Category, each with a folder icon.](https://cdn.sanity.io/images/3do82whm/next/e1c56e3e9f676cba374476185a66508d564688af-1277x361.png)
*Studio with a desk structure containing Settings, Post, Author, and Category document types*

## Creating a manual group for two filters

Before we create filters, we'll first create a manual group to house our two dynamic lists. For a review, read this article on [creating a manual group with Structure Builder](https://www.sanity.io/docs/studio/manually-group-items-in-a-pane).

Next, we'll create a new `listItem()` for our "Base" list. We'll give it the title "Filtered Posts" and a `.child()` node that will be a static list with the title "Filters."

This list will have two items, our filtered lists "Posts by Category" and "Posts by Author."

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(
          S.list()
            .title('Filters')
            .items([
              S.listItem().title('Posts By Category').child(),
              S.listItem().title('Posts By Author').child(),
            ])
          ),
      // The rest of this document is from the original manual grouping in this series of articles
      ...S.documentTypeListItems().filter(
        (listItem) => !['siteSettings', 'navigation', 'colors'].includes(listItem.getId())
      ),
      S.listItem()
        .title('Settings')
        .child(
          S.list()
            .title('Settings Documents')
            .items([
              S.listItem()
                .title('Metadata')
                .child(S.document().schemaType('siteSettings').documentId('siteSettings')),
              S.listItem()
                .title('Site Colors')
                .child(S.document().schemaType('colors').documentId('colors')),
              S.listItem()
                .title('Main Navigation')
                .child(S.document().schemaType('navigation').documentId('navigation')),
            ])
        ),
    ])
```

This will create a list of two items. Neither of those items will have children yet. To populate them, we'll use dynamic lists using [GROQ queries](https://www.sanity.io/docs/groq-reference).

## Creating dynamic children with an `S.documentList()` and a GROQ filter

To grab blog posts by category, we need to create a child for our `listItem` that will pull a `documentTypeList`. This list will show all categories in the dataset.

This will create a list of items with a document type that matches the string `'category'`. From here, we need to fill in what this item's child will be. In our case, we want to create a list of all the documents that match the category clicked. Replace the `child` after the “Posts By Category” `title` as shown below.

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(
          S.list()
            .title('Filters')
            .items([
              S.listItem()
                .title('Posts By Category')
                .child(
                  S.documentTypeList('category')
                  .title('Posts by Category')
                  .child(categoryId => 
                    S.documentList()
                      .title('Posts')
                      .filter('_type == "post" && $categoryId in categories[]._ref')
                      .params({ categoryId })
                    )
                  ),
              S.listItem().title('Posts By Author').child(),
            ])
        ),
     // .. rest of structure
    ])
```

The `.child()` method can accept an anonymous "arrow function", which will have the `_id` of the current item passed into it. From there, we need to define what type of child we're creating. 

### `S.documentList()`

The `.documentList()` method will pull a list of documents given a filter. It accepts most of the same chained methods as the `.list()` method but has a few special methods. 

### `.filter()`

The `.filter()` method is not the normal JavaScript filter method. In this case, it's a function that will accept a GROQ query as a string and return an array of documents that match that query. We can optionally chain a `.parameter()` method to pass a parameter into our query. In this case, the `categoryId` from our current function scope.

The GROQ query here will match all documents with a `_type` of `post`, containing the `$categoryId` as a reference in its `categories` array.

At this point, we have the post documents that match our query pulling into the next pane.

![A multi-column content management interface with "Filtered Posts", "Posts By Category", and "A category" selected, displaying a post titled "Testing a category".](https://cdn.sanity.io/images/3do82whm/next/3a4cf4a9c2cf455f39f1c4ed14922b65d7a3dfd3-1551x492.png)
*A list of documents matching our category filter in the final desk pane.*

### Adding "Posts by author" child node

Now, let's do the same process to pull posts by author reference into the "Post by Author" node.

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(
          S.list()
            .title('Filters')
            .items([
              S.listItem()
                .title('Posts By Category')
                .child(
                  S.documentTypeList('category')
                  .title('Posts by Category')
                  .child(categoryId => 
                    S.documentList()
                      .title('Posts')
                      .filter('_type == "post" && $categoryId in categories[]._ref')
                      .params({ categoryId })
                    )
                  ),
              S.listItem()
                .title('Posts By Author')
                .child(
                  S.documentTypeList('author')
                    .title('Posts by Author')
                    .child(authorId =>
                      S.documentList()
                      .title('Posts')
                      .filter('_type == "post" && $authorId == author._ref')
                      .params({ authorId })
                    )
                ),
            ])
        ),
     // .. rest of structure
    ])
```

![Screenshot of a user interface showing a content filtering workflow, with 'Filtered Posts', 'Posts By Author', and 'Bryan Robinson' selected, displaying one post titled 'Testing a category by Bryan Robinson'.](https://cdn.sanity.io/images/3do82whm/next/d7b136d69db1d0f49a963a2fcddb84bcdc3a2ceb-1551x483.png)
*A group of documents with Bryan Robinson as the author, grouped by our new filter.*

## Renaming the "Post" document type list item

Now that we have a "Filtered Posts" group, let's rename our "Post" document type list. To do this, we'll create a new manual list item in the "Base" list group. For a more in-depth explanation, see [this article on creating singleton documents](https://www.sanity.io/docs/studio/create-a-link-to-a-single-edit-page-in-your-main-document-type-list). In this example, we'll create a new `listItem()` for the post document type, give it a new title, and create a child panel with a document list filtering all posts. From there, we'll add the `'post'` ID to our exclusion filter for all other document types in this list.

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(/* Dynamic lists created earlier in here */ ),
      S.listItem()
        .title('All Posts')
        .child(
          // Create a list of all posts
          S.documentList()
            .title('All Posts')
            .filter('_type == "post"')
        ),
      ...S.documentTypeListItems().filter(
        (listItem) => !['post','siteSettings', 'navigation', 'colors'].includes(listItem.getId())
      ),
      S.listItem()
        .title('Settings')
        .child(
          S.list()
            .title('Settings Documents')
            .items([
              S.listItem()
                .title('Metadata')
                .child(S.document().schemaType('siteSettings').documentId('siteSettings')),
              S.listItem()
                .title('Site Colors')
                .child(S.document().schemaType('colors').documentId('colors')),
              S.listItem()
                .title('Main Navigation')
                .child(S.document().schemaType('navigation').documentId('navigation')),
            ])
        ),
    ])
```

This is beginning to look finished. We can help increase an editor's understanding of the grouping by adding dividers between the various sections.

## Create visual sections in the base list with `.divider()`

![A UI showing a "Base" menu with "Filtered Posts" selected and "Filter Posts by" options for category and author.](https://cdn.sanity.io/images/3do82whm/next/9fbffa62ad72a2aed01541f7abed0c59610eab66-896x408.png)
*Static dividers help create a flow for editors to know what document types go together.*

To group things together, we'll use the `S.divider()` method in our `.items()` array. We want to group "All Posts" and "Filtered Posts" together, then allow the rest of our document types to flow in the middle, then our "Settings." To do this, we'll insert the divider method in the order we want it to appear.

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(/* Dynamic lists created earlier in here */ ),
      S.listItem()
        .title('All Posts')
        .child(
          // Create a list of all posts
          S.documentList()
            .title('All Posts')
            .filter('_type == "post"')
        ),
      S.divider(),
      ...S.documentTypeListItems().filter(
        (listItem) => !['post','siteSettings', 'navigation', 'colors'].includes(listItem.getId())
      ),
      S.divider(),
      S.listItem()
        .title('Settings')
        .child(
          S.list()
            .title('Settings Documents')
            .items([
              S.listItem()
                .title('Metadata')
                .child(S.document().schemaType('siteSettings').documentId('siteSettings')),
              S.listItem()
                .title('Site Colors')
                .child(S.document().schemaType('colors').documentId('colors')),
              S.listItem()
                .title('Main Navigation')
                .child(S.document().schemaType('navigation').documentId('navigation')),
            ])
        ),
    ])
```

## Final code

Putting together all the examples from this series of articles we get the following desk structure.

**structure.ts**

```typescript
import {type StructureResolver, type StructureBuilder} from 'sanity/structure'

export const myStructure: StructureResolver = (S: StructureBuilder) =>
  S.list()
    .title('Base')
    .items([
      S.listItem()
        .title('Filtered Posts')
        .child(
          S.list()
            .title('Filters')
            .items([
              S.listItem()
                .title('Posts By Category')
                .child(
                  S.documentTypeList('category')
                  .title('Posts by Category')
                  .child(categoryId => 
                    S.documentList()
                      .title('Posts')
                      .filter('_type == "post" && $categoryId in categories[]._ref')
                      .params({ categoryId })
                    )
                  ),
              S.listItem()
                .title('Posts By Author')
                .child(
                  S.documentTypeList('author')
                    .title('Posts by Author')
                    .child(authorId =>
                      S.documentList()
                      .title('Posts')
                      .filter('_type == "post" && $authorId == author._ref')
                      .params({ authorId })
                    )
                ),
            ])
      ),
      S.listItem()
        .title('All Posts')
        .child(
          // Create a list of all posts
          S.documentList()
            .title('All Posts')
            .filter('_type == "post"')
        ),
      S.divider(),
      ...S.documentTypeListItems().filter(
        (listItem) => !['post','siteSettings', 'navigation', 'colors'].includes(listItem.getId())
      ),
      S.divider(),
      S.listItem()
        .title('Settings')
        .child(
          S.list()
            .title('Settings Documents')
            .items([
              S.listItem()
                .title('Metadata')
                .child(S.document().schemaType('siteSettings').documentId('siteSettings')),
              S.listItem()
                .title('Site Colors')
                .child(S.document().schemaType('colors').documentId('colors')),
              S.listItem()
                .title('Main Navigation')
                .child(S.document().schemaType('navigation').documentId('navigation')),
            ])
        ),
    ])
```

## Next steps

Now that we've created singletons, static lists, and dynamic lists, we need to look at [creating tabs and custom previews for our document views](https://www.sanity.io/docs/studio/create-custom-document-views-with-structure-builder).





