✨Discover storytelling in the AI age with Pixar's Matthew Luhn at Sanity Connect, May 8th—register now


A dedicated, preview-first space for content creation.

The Presentation tool enables Visual Editing in the Sanity Studio and lets you work with content directly through preview — be it a website, in-store display, or anything you can point a browser at.

You can see what parts of the page are editable, click an element, and be brought directly to the content field to make your change. This complements the Structure tool (previously named “Desk”), where you find the right field through search, following references, and expanding modals for nested fields.

Presentation saves time when working with visually oriented layouts and complex content models. Using Presentation gets you right to where you want to edit and lets you save effort by using structured content extensively (without the penalty of figuring out where documents are used).

You're probably using structured content – so how do you find the many places a document is re-used? The Document Locations Resolver lets developers declare where a document is used in your front-end presentation(s). This makes content edits predictable and safe.

Presentation offers the following features for content creators:

  • Interactive, navigable in-Studio preview
  • Live-as-you-edit preview updates
  • Option to switch between draft and published mode to check your changes against what's currently live
  • Comments for in-context feedback and reviews, with notifications and status.
  • Overview of content reuse across front ends
  • Wide and narrow views to see your work on different devices
  • Deep link to edit states for bookmarking and collaboration
  • Scroll syncing between preview display and content fields in the document editor

Basic configuration

This article details the configuration options for the Presentation tool. We recommend checking out these articles if you are setting up Visual Editing:


To use the Presentation tool, you have to upgrade your Studio‘s sanity dependency to v3.20.0 or preferably latest.

You might also want to install rxjs as a dependency to your Studio project to enable real-time document locations with Observables:

npm install sanity@latest rxjs

Similarly to the Structure tool (formerly the "Desk tool"), you add Presentation to your Studio's configuration file (code example below).

Basic configuration

  1. Import presentationTool from sanity/presentation
  2. Add it to the plugins array
  3. Add the base URL for your front end to the previewUrl attribute
// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
// ...other imports // We recommend configuring the preview location base URL using // environment variables to support multiple environments
|| 'http://localhost:3000'
export default defineConfig({ // Your configuration for the project ... plugins: [ // Add 'presentationTool' to the 'plugins' array
// Required: set the base URL to the preview location in the front end
// ...other plugins ], })

Configuration reference

The Presentation tool takes the following configuration attributes:

  • REQUIREDpreviewUrlstring

    The relative or absolute path to the base location in the front end you want to enable in the Presentation tool. If your Studio shares the same base URL as your front end, you can use relative URLs. We recommend implementing this as an environment variable to make it easier to work in development and production.


    • http://localhost:3000
    • https://your-production-url.com/
    • /

    You may also configure the Presentation tool to handle enabling/disable draft automatically.

  • iconReact component

    Sets an icon to represent the Presentation tool in the Studio's navigation bar. To use Sanity icons, install the @sanity/icons package.

  • namestring

    Sets a name for the Presentation tool. It's not visually represented in the Studio UI, but it’s included in the Studio's URL for the Presentation tool.

    This is useful if you want to implement multiple instances of Presentation, for example, for different channels, domains, or preview environments.

    Default value: presentation

    Examples: presentation, preview, website, app

  • titlestring

    Sets the title that is displayed in the Studio's navigation bar. Keep it short and descriptive to help editorial users understand what they can do with the tool.

    Default value: Presentation

    Examples: Website Preview, Newsletter Preview, US Site Preview

  • locateObservable<DocumentLocationsState>

    This takes a callback function that returns a list of document locations. These are typically the URLs where content from a selected document is used.

    For more information about the locate callback, see Configure Location resolver below.

Configuring Document Locations

Typically, a page on your front end will contain content from multiple documents when using structured content. For example, a marketing landing page may include content from a landing page document as well as content from referenced products, people, shared content modules, and more. You can also save time and effort by having documents spanning multiple locations across several digital channels.

The Document Locations Resolver generates a list of the locations where a document is used. This helps editors understand how a change in a single document can affect the different places it is used.

The Locations UI showing where a product has been used

The locate callback property is a resolver that takes a document id and type parameters, and returns a state object that contains the locations that can be previewed for a given document.

Basic example

The example below shows you how to implement document locations for a simple blog post document type where its id is used as part of the URL scheme:

// locate.ts

export const locate: DocumentLocationResolver = ({id, type}) => {
	// Set up locations for documents of the type "post"
  if (type === 'post') {
    return {
      // '/post' is an example path.
      // Replace it with an actual relative or absolute value
      // depending on your environment
      locations: [
        {title: `Post #${id}`, href: `/post/${id}`},
        {title: 'Posts', href: '/posts'},
  return null

We recommend containing the Document Location Resolver in a dedicated file to reduce clutter in your Studio configuration file. In the example above, it's exported as a named JavaScript variable so it can be imported to the studio configuration file like this:

// sanity.config.ts
import {defineConfig} from 'sanity'
import {presentationTool} from 'sanity/presentation'
import {locate} from './locate'
// ...other imports export default defineConfig({ // ...configuration for the project plugins: [ presentationTool({ previewUrl: SANITY_STUDIO_PREVIEW_URL,
}), // ...other plugins ], })

Generate locations based on document data

Most often, URLs are built up from document fields (like slug.current). The callback passes a second context parameter with a function that has methods for interacting with documents in real time. In most cases, you will call context.documentStore.listenQuery with a GROQ query that returns the queried documents as an Observable.

We recommend installing rxjs as a dependency in your Studio project to make it easier to interact with the Observable object returned from listenQuery:

npm install rxjs

Now, you can use the parameters you get from the selected document to access the document data and generate the URLs based on it. Below is an example of how to do this for a blog post document where the slug field is used for URLs:

// locate.ts
import { DocumentLocationResolver } from "sanity/presentation";
import { map } from "rxjs";

// Pass 'context' as the second argument
export const locate: DocumentLocationResolver = (params, context) => {
  // Set up locations for post documents
  if (params.type === "post") {
    // Subscribe to the latest slug and title
    const doc$ = context.documentStore.listenQuery(
      `*[_id == $id][0]{slug,title}`,
      { perspective: "previewDrafts" } // returns a draft article if it exists
    // Return a streaming list of locations
    return doc$.pipe(
      map((doc) => {
        // If the document doesn't exist or have a slug, return null
        if (!doc || !doc.slug?.current) {
          return null;
        return {
          locations: [
              title: doc.title || "Untitled",
              href: `/post/${doc.slug.current}`,
              title: "Posts",
              href: "/",
  return null;

Show all locations where a document is being used

Typically, with structured content, content from a document might be used in multiple locations by means of references. With GROQ, you can query for all documents that refer to a specific document (or use other join logic) and use that to build a list of locations for where a document is being used.

Below is an example showing how to build a list of locations for a "person" document that is being referred to by a "post" document as its author:

// locate.ts
import {
} from 'sanity/presentation'
import { map, Observable } from 'rxjs'

export const locate: DocumentLocationResolver = (params, context) => {
  if (params.type === 'post' || params.type === 'person') {
      Listen to all changes in the selected document 
      and all documents that reference it
    const doc$ = context.documentStore.listenQuery(
      `*[_id==$id || references($id)]{_type,slug,title, name}`,
      { perspective: 'previewDrafts' },
    ) as Observable<
      | {
          _type: string
          slug?: { current: string }
          title?: string | null
          name?: string | null
      | null
    // pipe the real-time results to RXJS's map function
    return doc$.pipe(
      map((docs) => {
        if (!docs) {
          return {
            message: 'Unable to map document type to locations',
            tone: 'critical',
          } satisfies DocumentLocationsState
        // Generate all the locations for person documents
        const personLocations = docs
          .filter(({ _type, slug }) => _type === 'person' && slug?.current)
          .map(({ name, slug }) => ({
            title: name || 'Name missing',
            href: `/authors/${slug.current}`,

        // Generate all the locations for post documents
        const postLocations: Array<any> = docs
          .filter(({ _type, slug }) => _type === 'post' && slug?.current)
          .map(({ title, slug }) => ({
            title: title || 'Name missing',
            href: `/posts/${slug.current}`,

        return {
          locations: [
            // Add a link to the "All posts" page when there are post documents
            postLocations.length > 0 && {
              title: 'All posts',
              href: '/posts',
            // Add a link to the "All authors" page when there are person documents
            personLocations.length > 0 && {
              title: 'All authors',
              href: '/authors',
        } satisfies DocumentLocationsState

  return null

Document Location Resolver Reference

In addition to the example above, the Document Location Resolver can return customized top-level messages and visual cues:

  • messagestring

    Override the top-level text in the document locations UI. Useful if you want to customize the string (replace "pages") or display warnings.

    Default value: Used on ${number} pages


    • Unable to resolve locations for this document
    • Used in ${docs.length} email campaign${docs.length === 1 ?? 's'}
  • tone'positive' | 'caution' | 'critical'

    Gives the document locations UI a background color. It can be used to signal criticality.

    Default: positive

  • locationsDocumentLocation[]

    An array of document locations objects with title and href properties. The href can be absolute or relative and will open in the Presentation tool.

Mount a custom navigator component

Optionally, you can enhance the Presentation tool with a custom document navigator component to help users select different documents or views in the front-end UI.

Unstable feature

unstable_navigator has the unstable prefix because the API is likely to change. Don’t use it in a production environment.


// Import your custom navigator component
import {NavigatorComponent} from './presentation/NavigatorComponent'

export default defineConfig({
  // Your configuration for the project
  // ...

  plugins: [
      // ...
      component: {
  			// Pass the custom component to the plugin
        unstable_navigator: NavigatorComponent

Navigator properties

  • REQUIREDcomponentReact component

    Specifies the navigator component to use with the Presentation tool. The component specified will be rendered as the content of the navigator panel.

  • minWidthnumber

    Sets the minimum width of the navigator component when it’s rendered in the UI. For the component to render, its value must be > 0 and other than null.

  • maxWidthnumber

    Sets the maximum width of the navigator component when it’s rendered in the UI. For the component to render, its value must be > 0 and other than null.

Was this article helpful?