Unlock seamless workflows and faster delivery with our latest releases- get the details
Experimental feature

Custom overlay components and controls

Visual editing overlays can be extended with custom React components. These components are typically used to enable direct in-app content editing and display content metadata to editors.

Custom overlay components allow you to extend the functionality of visual editing overlays with React components. These components enhance the editing experience by enabling direct in-app content editing and displaying metadata or controls to content editors.

With custom overlays, you can:

  • Add interactive controls such as color pickers or sliders to configure complex objects (e.g., 3D models).
  • Display additional context, such as related product data from external systems.

You can also customize the Presentation tool's preview header, giving you the flexibility to toggle custom overlays, or add controls, status indicators, or other UI elements that enhance the editor experience.

Prerequisites

Before getting started, ensure the following:

  • Visual Editing enabled with up-to-date dependencies in your front end
  • Sanity Studio v3.65.0 or later (npm install sanity@latest)

Gotcha

Custom overlay component arcurrently only supported for React.

Custom overlay components

You can mount any React component in an overlay, but the OverlayComponent type provides type safety. Below is a simple example that renders the name of the field associated with the overlay:

// ./overlay-components.tsx
"use client"
import {type OverlayComponent} from '@sanity/visual-editing'

export const FieldNameOverlay: OverlayComponent = ({field}) => (
  <div className="absolute bottom-0 left-0 m-1 rounded bg-black bg-opacity-50 px-2 py-1 text-xs text-white">
    {field?.name}
  </div>
)

Gotcha

Custom overlay components and resolvers should be rendered client-side, commonly done with the "use client" directive for React.

Using custom overlay component resolvers

Resolvers determine which custom components to mount for specific overlays. Use the defineOverlayComponents helper to conditionally resolve components based on overlay context.

This function runs each time an overlay renders, and the context object it receives can be used to determine which components to return.

Resolver functions can return:

  • JSX elements.
  • React component(s), single or array.
  • Object(s) with component and props values. Use the defineOverlayComponent for convenience and type safety, single or array.
  • undefined or void when no custom components should be mounted.

Below is an example for how to resolve different custom overlay components conditionally:

// ./component-resolver.tsx
"use client"
import {
  defineOverlayComponent,
  defineOverlayComponents,
} from '@sanity/visual-editing/unstable_overlay-components'
import {TitleControl, HighlightOverlay, UnionControl, UnionTypeMarker} from './overlay-components.tsx'

export const components = defineOverlayComponents((context) => {
  const {document, element, field, type, parent} = context

  // Mount a component in overlays attached to string
  // fields named 'title'
  if (type === 'string' && field.name === 'title') {
    return TitleControl
  }

  // Return JSX directly
  if (type === 'string' && field.name === 'subtitle') {
    return <div>Subtitle</div>
  }

  // Mount a component in overlays attached to any element
  // corresponding to a 'product' document
  if (document.name === 'product') {
    const color = element.dataset.highlightColor || 'red'
    return defineOverlayComponent(HighlightOverlay, {color})
  }

  // Mount multiple components in overlays attached to any
  // member element of a union type
  if (parent?.type === 'union') {
    return [
      UnionTypeMarker,
      defineOverlayComponent(UnionControl, { direction: 'vertical' })
    ]
  }

  return undefined
})

Depending on your framework and implementation, the resolver function should be passed via the components property of the object passed to the enableVisualEditing function, or the components prop of the <VisualEditing> component. For example:

// app/(website)/layout.tsx
import { VisualEditing } from "next-sanity";
import { draftMode } from "next/headers";
import { components } from "./component-resolver.tsx";

// minimal Next.js-like example
export default async function RootLayout({ children }) {
  return (
    <html>
      <body>
        <main>{children}</main>
        {isDraftMode && <VisualEditing components={components} />}
      </body>
    </html>
  );
}

Using custom overlay controls

Custom overlay controls enable powerful editing capabilities directly in your application, from basic string manipulation to advanced controls for controlling 3D scenes.

Protip

Custom overlay controls will automatically use the logged-in user's authentication to update content. This means that any permissions that the user has will still be respected.

Install the @sanity/mutate package in your front-end project to create the necessary patches for updating data. Refer to that package’s documentation for available methods.

npm install @sanity/mutate

The example below illustrates to mount a button in an overlay which appends an exclamation mark on the end of a string value when clicked.

// ./overlay-components.tsx
"use client"
import {at, set} from '@sanity/mutate'
import {get} from '@sanity/util/paths'
import {useDocuments, type OverlayComponent} from '@sanity/visual-editing'

export const ExcitingStringControl: OverlayComponent = (props) => {
  const {node, PointerEvents} = props
  // Get the document ID and field path from the Sanity node.
  const {id, path} = node;

  const {getDocument} = useDocuments()
  // Get the optimistic document using the document ID.
  const doc = getDocument(id)

  const onClick = () => {
    doc.patch(async ({getSnapshot}) => {
      const snapshot = await getSnapshot()
	    // Get the current value using the document snapshot and the field path.
      const currentValue = get<string>(snapshot, path)
      // Return early if the string is already exciting.
      if (currentValue?.endsWith('!')) return []
      // Append "!" to the string.
      const newValue = `${currentValue}!`
      // Use `@sanity/mutate` functions to create the document patches.
      return [at(node.path, set(newValue))]
    })
  }

  return (
    // By default, overlays don't receive pointer events.
    // Use the `PointerEvent` wrapper component to allow interaction.
    <PointerEvents>
      <button
        // Tailwind CSS classes
        className="absolute right-0 rounded bg-blue-500 px-2 py-1 text-sm text-white"
        onClick={onClick}
      >
        🎉
      </button>
    </PointerEvents>
  )
}

Access custom preview header state in custom overlay components

The visual editing package exports an named useSharedState hook which given the unique key defined in a custom preview header (for example, highlighting), will return the value shared by the corresponding useSharedState Presentation tool hook.

Below, we have added the custom highlighting overlay component to the custom overlays file that is rendering a semi-transparent overlay when highlighting is enabled, and nothing when disabled:

// ./overlay-components.tsx
"use client"
import {useSharedState, type OverlayComponent} from '@sanity/visual-editing'

export const FieldNameOverlay: OverlayComponent = (props) => {
  const {field} = props

  return (
	  // Tailwind CSS classes
    <div className="absolute bottom-0 left-0 m-1 rounded bg-black bg-opacity-50 px-2 py-1 text-xs text-white">
      {field?.name}
    </div>
  )
}

export const HighlightOverlay: OverlayComponent = () => {
  const highlight = useSharedState<boolean>('highlighting')

  if (!highlight) {
    return null
  }

  return (
    <div
      style={{
        position: 'absolute',
        inset: 0,
        backgroundColor: 'rgba(0, 0, 255, 0.25)',
      }}
    />
  )
}

Hooks reference

useDocument

  • useDocuments(): { getDocument, mutateDocument }

    The useDocuments hook can be used in overlay components to access and update the documents currently in use on a page.

  • getDocument(documentId): { id, get, patch, commit }

    Returns an optimistic document interface with the following methods:

    • id: string - The document ID.
    • get: (path?: string): SanityDocument | PathValue - Returns the document snapshot or the specific value at the given path.
    • patch: (patches: OptimisticDocumentPatches, options?: {commit?: boolean | {debounce: number}}) => void - Applies patches to the document, will commit patches by default.
    • commit: () => void - Commits pending changes.

    Parameters

    • documentIdstring

      The ID of the document to get.

  • mutateDocument(documentID, mutations, options): void

    Parameters

    • documentIDstring
      • The ID of the document to mutate.
    • mutationsMutation[]

      The mutations to apply to the document.

    • options{ commit: boolean | {debounce: number }}

      Optional commit options.

useSharedState

  • useSharedState(key, value): Your serializeable state

    The useSharedState enables you to share state between the Presentation tool and your custom overlay components in your front end’s preview.

    Parameters

    • keystring

      Acts as a unique identifier for the shared state within the context. This key is used to associate a specific state value with a logical “slot” in the shared state object.

      Best practice:

      • Use descriptive and unique keys to avoid conflicts between multiple shared states.
      • Keys should be stable (i.e., not dynamically generated) to ensure predictable behavior.

      Example: useSharedState('highlighting', true);

    • valueA serializeable state

      Represents the state value associated with the given key. This value will be shared with other components that query the state using the same key.

      Requirements: Must be JSON serializable (string, number, boolean, null, arrays, or plain objects) to ensure compatibility with mechanisms like serialization, storage, or sharing across contexts.

      Best practices:

      • Ensure the value is minimal and only includes the necessary data.
      • Avoid passing complex or deeply nested structures to keep the shared state manageable.

Resources

Was this article helpful?