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.
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.
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.
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
andprops
values. Use thedefineOverlayComponent
for convenience and type safety, single or array. undefined
orvoid
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>
);
}
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>
)
}
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)',
}}
/>
)
}
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(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 plainobjects
) 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.