
Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag storeYes, you can definitely build a directory tree structure in Sanity! The key is using a parent reference field on both your directory documents and content documents, then leveraging the Structure Builder's child resolvers to create the navigation hierarchy.
First, create a directory document type with a parent reference to itself:
// schemas/directory.ts
export default {
name: 'directory',
type: 'document',
title: 'Directory',
fields: [
{
name: 'title',
type: 'string',
title: 'Directory Name'
},
{
name: 'parent',
type: 'reference',
title: 'Parent Directory',
to: [{type: 'directory'}],
description: 'Leave empty for root-level directories'
}
]
}Then add a parent reference to your content documents:
// schemas/article.ts (or whatever your type A is)
export default {
name: 'article',
type: 'document',
fields: [
// ... your existing fields
{
name: 'parent',
type: 'reference',
title: 'Parent Directory',
to: [{type: 'directory'}],
description: 'Organize this document in a directory'
}
]
}Here's how to build the tree view using child resolvers. The important thing to understand is that child resolvers respond to user navigation - when someone clicks on a directory, the child() function determines what shows in the next pane:
// structure/index.ts
import type {StructureResolver} from 'sanity/structure'
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
S.listItem()
.title('📁 Content Tree')
.child(
S.list()
.title('Root')
.items([
// Root-level directories
S.listItem()
.title('Directories')
.child(
S.documentList()
.title('Root Directories')
.filter('_type == "directory" && !defined(parent)')
.child((documentId) =>
// This child resolver is called when a directory is clicked
S.list()
.title('Contents')
.items([
// Subdirectories in this directory
S.listItem()
.title('📁 Subdirectories')
.child(
S.documentList()
.title('Subdirectories')
.filter(`_type == "directory" && parent._ref == $parentId`)
.params({parentId: documentId})
.child((childDocId) =>
// Recursive navigation - same structure for subdirectories
S.list()
.title('Contents')
.items([
S.listItem()
.title('📁 Subdirectories')
.child(
S.documentList()
.title('Subdirectories')
.filter(`_type == "directory" && parent._ref == $parentId`)
.params({parentId: childDocId})
// You can nest this pattern as deep as needed
),
S.listItem()
.title('📄 Documents')
.child(
S.documentList()
.title('Documents')
.filter(`_type == "article" && parent._ref == $parentId`)
.params({parentId: childDocId})
),
S.divider(),
S.documentListItem()
.id(childDocId)
.schemaType('directory')
.title('Edit this directory')
])
)
),
// Documents in this directory
S.listItem()
.title('📄 Documents')
.child(
S.documentList()
.title('Documents')
.filter(`_type == "article" && parent._ref == $parentId`)
.params({parentId: documentId})
),
S.divider(),
// Allow editing the directory itself
S.documentListItem()
.id(documentId)
.schemaType('directory')
.title('Edit this directory')
])
)
),
// Root-level documents (no parent)
S.listItem()
.title('📄 Documents')
.child(
S.documentList()
.title('Root Documents')
.filter('_type == "article" && !defined(parent)')
)
])
),
S.divider(),
// Keep flat views as backup for finding content
S.documentTypeListItem('directory').title('All Directories'),
S.documentTypeListItem('article').title('All Articles'),
])The Structure Builder's child resolver pattern works by responding to user navigation. When someone clicks on a directory in the list:
child() function receives the clicked document's IDparent._ref == $parentId find children of each directory*[_type == "article" && _id == $id][0] {
_id,
title,
parent->{
title,
parent->{
title,
parent->{title}
}
}
}The example above shows nesting 2-3 levels deep explicitly. For deeper nesting, you'd want to extract the list-building logic into a reusable function that you can call at each level, but the pattern remains the same: use child() with the documentId to create filtered lists of subdirectories and documents.
This gives your editors a familiar folder-like browsing experience while maintaining Sanity's flexible document-based architecture!
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.
Content operations
Content backend


The only platform powering content operations
By Industry


Tecovas strengthens their customer connections
Build and Share

Grab your gear: The official Sanity swag store
Read Grab your gear: The official Sanity swag store