🔮 Sanity Create is here. Writing is reinvented. Try now, no developer setup

Overlays

Overlays is the part of Visual Editing that creates interactivity between elements in an application and the Sanity Studio. They enable click-to-edit, which takes you directly from content on your front end to the document where you can edit it. Overlays can be automatically enabled for most text-based content using Stega strings and manually added to your components with helper functions.

This article unpacks how overlays work, how to set them up manually, and how to troubleshoot styling.

How overlays work

Overlays are part of the Visual Editing component that you usually integrate from a framework toolkit. For overlays to work, they need to do two things:

  • Identify what elements in your DOM (that is, the HTML in your front end) have content sourced from your Sanity Content Lake to enable the hover/focus effect
  • Set up the correct mapping so that when you click the overlay, you are taken to the proper document and field in the Studio, where you can edit it.

Most CMSes with visual editing functionality solve this in two ways:

  • They assume that a page sources content from just one document
  • They require you to use their opinionated component system or require you to manually mark up every component with data attributes with the mapping to the CMS

We found these approaches too limiting and inflexible because:

  • You might have content from multiple documents on a given page, especially when using GROQ and GraphQL
  • You should be able to accommodate Visual Editing with any framework and component system
  • A component might be used in several contexts and have different content sources, and it’s a lot of work to manually map them up

The Visual Editing tooling solves this by:

  • Adding Content Source Maps (CSM) to query responses that map the content in a request to the documents and fields they’re sourced from
  • Encoded CSM into Stega strings, that is, invisible characters that are added to text strings and read by the overlays package to generate the visual affordances and click-to-edit functionality

You can also use CSM and built-in functions to manually add overlays to elements with non-text-string content types.

Overlays from Stega encoded strings

Stega is an encoding method we developed alongside Vercel to provide an easy-to-implement mechanism for automatically embedding metadata mappings into an application. It uses Content Source Maps, a standard we developed for annotating fragments in a JSON document with metadata about their origin.

Protip

With Stega enabled, although data rendered in our application looks exactly the same, it contains invisible metadata that Sanity’s Visual Editing tooling can detect.

Stega encoding is usually enabled at the client or loader level, depending on your framework.

Enabling overlays manually

Stega encoding only works for text strings, including alternative text for images. In some cases, you want to manually map content in a component to its source.

The two manual methods involve inserting Sanity specific data attributes into our application:

  • createDataAttribute takes a document ID, type, and field path
  • encodeDataAttribute works with a loader and takes a field name

createDataAttribute

createDataAttribute is a function that uses content metadata to create data-sanity attributes.

At a minimum, you need to provide a document id, a document type, and the path to the field the data originated from. Below is a straightforward and illustrative example of how we might create a React component that displays a title with a data-sanity attribute.

import { createDataAttribute } from "@sanity/visual-editing";

export function TitleComponent(props: {
  title: string;
  documentId: string;
  documentType: string;
  fieldPath: string;
}) {
  const { title, documentId, documentType, fieldPath } = props;

  const attr = createDataAttribute({ 
    id: documentId, 
    type: documentType, 
    path: fieldPath 
	});

  return <h1 data-sanity={attr()}>{title}</h1>;
}

Note that the result of our createDataAttribute call is a function. This is because this API also allows for incrementally scoping and reusing attributes. We can also provide further optional metadata, for example, to target a particular workspace. See the API reference for more details.

import { createDataAttribute } from "@sanity/visual-editing";

export function HeaderComponent(props: { document: SanityDocument }) {
  const { document } = props;

  // Create a data attribute scoped to the document and a custom workspace
  const attr = createDataAttribute({
    id: document._id,
    type: document._type,
    workspace: 'staging'
  });

  // Pass field names into the attr() function
  return (
    <div>
      <h1 data-sanity={attr("title")}>{document.title}</h1>
      <h2 data-sanity={attr("subtitle")}>{document.subtitle}</h2>
    </div>
  );
}

encodeDataAttribute

encodeDataAttribute is a loader specific API, it can be thought of as a pre-scoped or context aware version of createDataAttribute. Depending on the loader used, it is usually returned by a useQuery hook or composable. As document context is provided by the loader itself, it only requires a field path to be explicitly defined.

Below is an example of how encodeDataAttribute can create data attributes for multiple field values in a React client component.

"use client";

import { QueryResponseInitial, useQuery } from "@sanity/react-loader";
import { actorQuery, type ActorResult } from "./queries";

type Props = {
  params: { slug: string };
  initial: QueryResponseInitial<ActorResult>;
};

export default function MoviesByActor(props: Props) {
  const { initial, params } = props;
  const { data: actor, encodeDataAttribute } = useQuery<ActorResult>(
    actorQuery,
    params,
    { initial }
  );

  return (
    <div>
      {/* Create an attribute for the actor's name */}
      <h1 data-sanity={encodeDataAttribute(["name"])}>
        Movies featuring {actor.name}
      </h1>

      {actor.movies.map((movie, index) => (
        <ul key={movie._id}>
          {/* Use the index to scope to each item in the array of movies */}
          <li data-sanity={encodeDataAttribute([index, "title"])}>
            {movie.title}
          </li>
        </ul>
      ))}
    </div>
  );
}

Check the documentation specific to the loader you are using for more information about encodeDataAttribute.

Troubleshooting

Seeing weird characters in your DOM or things that work in prod but not in preview mode, you might want to check out the Stega docs

Styling of editable fields is incorrect

If the text on the page is breaking out of its container – or its container is much wider than normal – it can be resolved by splitting the encoded text out from the original text.

Note: This is not due to the encoded characters themselves. This problem should only present itself if the element also uses negative letter-spacing in its CSS or is inside of a <ReactWrapBalancer>.

Then identify where the problematic element is rendered in code, for example:

export function MyComponent({ text }: { text: string }) {
  return <h1>{text}</h1>;
}

Rewrite using @vercel/stega to avoid any styling issues:

import { vercelStegaSplit } from "@vercel/stega";

export function MyComponent({ text }: { text: string }) {
  const { cleaned, encoded } = vercelStegaSplit(text);

  return (
    <h1>
      {cleaned}
      <span style={{ display: "none" }}>{encoded}</span>
    </h1>);
}

If you find yourself doing this more than once, you might like to extract this logic to a reusable component:

import { vercelStegaSplit } from "@vercel/stega";

export default function Clean({ value }: { value: string }) {
  const { cleaned, encoded } = vercelStegaSplit(value);

  return encoded ? (
    <>
      {cleaned}
      <span style={{ display: "none" }}>{encoded}</span>
    </>) : (
    cleaned
  );
}

export function MyComponent({ text }: { text: string }) {
  return (
    <h1>
      <Clean value={text} />
    </h1>);
}

Overlay displays over the wrong element

If the wrong element is highlighted when hovering, an additional attribute can be added to a containing element.

For example, if the following component highlights the <h1> and you want it to highlight the <section> element:

<section>
	<h1>{dynamicTitle}</h1>
	<div>Hardcoded Tagline</div>
</section>

Add a data attribute to highlight the correct item:

  • For Visual Editing with @sanity/visual-editing, add data-sanity-edit-target
  • For Vercel Visual Editing, add data-vercel-edit-target
<section data-sanity-edit-target>
	<h1>{dynamicTitle}</h1>
	<div>Hardcoded Tagline</div>
</section>

Not required for Vercel visual editing

If using Vercel's visual editing, overlays will appear automatically when the Vercel Toolbar is visible – and the page contains stega encoding – such as in preview builds.

You may still wish to configure overlays, but only have them enabled when your front end is rendered inside an iframe, so they work within Presentation.

Was this article helpful?