👀 Our most exciting product launch yet 🚀 Join us May 8th for Sanity Connect

Loaders and Overlays

Make your front end Presentation-aware.

This guide article introduces you to the concepts of Loaders and Overlays and how to implement them in a front end. They are necessary parts to enable Visual Editing in Presentation for your content team.

What are Loaders

Loaders provide a convenient, unified way of loading data from Content Lake: A single front end data fetching implementation across production, development and preview states, for both server and client-side rendering.

We built Loaders to make it easier to integrate full-fledged Visual Editing functionality in your front end with less maintenance and repeated code. Loaders handle the heavy lifting of implementing Visual Editing into your front end by:

  • Supporting rendering Overlays in your application, using Content Source Maps (CSM) to map your content to the exact documents and fields it came from.
  • Enabling seamless switching into preview mode, using Perspectives to display either published or draft content.
  • Setting up near-instant, as-you-type updates in preview mode.

Crucially, loaders only run preview code paths in preview states, so your production site is kept fast and lean.

Loaders are available for several frameworks:

These framework-specific loaders are built on top of a foundational Core Loader that can be used in any JavaScript-based project.

What are Overlays

Overlays can be added using a separate package that works in conjunction with Loaders. When Loaders are configured to return data with stega encoding (more on this below), the Overlays package creates interactive links from each individual piece of data on the page back to the Sanity Studio, highlighting the document and field it came from.

Not required for Vercel visual editing

If using Vercel's visual editing, these 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.

React implementation overview

Configuring Loaders and Overlays is slightly different depending on your front end. For complete templates, guides, and example code for Next.js and Remix, see Guides and examples on the Presentation page.

Those examples could be generalized by the following diagram, showing where on the server or client React Loader functions should be called, and under which circumstances should be imported.

Generalized implementation of server-side and client-side functions from React Loader

The following is a brief overview of the functions contained in React Loader.

Required packages

You can install the required packages with npm, or your preferred package manager:

npm install @sanity/react-loader
npm install @sanity/client
npm install @sanity/overlays

Loading content into your front end

The library exports three utilities:

  • loadQuery : A loading function that returns a promise with query response and Content Source Map data. This is typically used to fetch content during server-side-rendering (SSR), which can then be used to initially hydrate the data returned by the useQuery hook.
  • useQuery: A React hook for loading and streaming content data on the client side. The useQuery hook returns an object with data, error, and loading properties:
    • data contains any initial data passed into the hook – such as the loadQuery's server-side response – and when liveMode is active, and updates the client-side state with fresh data from the query results.
    • error contains any errors that may have occurred when executing the query.
    • loading indicates whether the query is still in progress.
  • useLiveMode: a React hook that enables real-time updates for the queried data from the server to the client. It returns liveMode, a boolean value indicating whether the studio is in live preview mode. This is useful for scenarios where you want to adapt your UI based on whether the editor is in live preview. Any changes to the data in the Content Lake are reflected in the application without requiring a manual refresh. This enables applications to provide a seamless and responsive user experience with up-to-date data.

Enabling Visual Editing

The useLiveMode hook is typically only executed inside a component you'll create (often named VisualEditing). Which should only be lazily imported into your application when preview mode is required.

The existence of this component signals to the rest of the application – and any useQuery hooks – that live updates will be performed and that all content should be encoded with "Stega."

See the Visual Editing with Next.js App Router guide for an example VisualEditing component.

Render overlays for click-to-edit

You need to add one more piece to complete the puzzle: render elements that help users visually identify editable elements on a page so that they can click and edit them in the studio.

To make this happen, you need the Content Source Maps (CSM) to make it into your front end.

For simple strings, you can do this automatically with Stega. Stega is a method to encode metadata inside of text strings that we invented in collaboration with Vercel. It encodes the CSM steganographically into as invisible characters strings in your front end. Your visitors won't see these encodings, but the Overlays will see them and overlay the editing UI on hover.

Overlays render interactive visual indicators over your DOM elements. When they are clicked, we'll signal the Studio to display the right field, even deeply nested ones, we'll take the user right to it. Similarly, when a user clicks a field in a form - we'll scroll the DOM node into view if it exists on the page.

We recommend providing a HistoryAdapter to keep the studio preview pane in sync with your application router or history state. HistoryAdapter Implementations may differ depending on the framework you use. In any case, they must provide two methods:

  • subscribe: receives a navigate parameter, which is a function to execute when a new HistoryUpdate is sent to the preview frame.
  • update: is called when the preview frame history changes. It receives a HistoryUpdate parameter to push/pop/replace your application history.

Implementing overlays with data attributes

While Stega works for simple string values, you might want to enable overlays for more complex elements (like Product Card or Images) or for the cases where Stega strings don't work.

Overlays also work for elements that have a data-sanity attribute with the necessary information for where the field and studio are.

To generate this data object, you can use the encodeDataAttribute from the useQuery hook (in React front ends) and pass the field path for a given document type.

// Example from a Navigation component in a Next.js project

"use client";

import { type QueryResponseInitial } from "@sanity/react-loader/rsc";
import Link from "next/link";
import { NAV_QUERY } from "@/sanity/lib/queries";
import { resolveHref } from "@/sanity/lib/resolveHref";
import { useQuery } from "@/sanity/loader/useQuery";
import { NavigationData } from "@/types";
import { useCallback } from "react";
import { useEncodeDataAttribute } from "@sanity/react-loader";
import { STUDIO_ORIGIN } from "@/sanity/store";

type Props = {
  initial: QueryResponseInitial<NavigationData>;
};

export function Navigation(props: Props) {
  const { initial } = props;
  const { data, sourceMap } = useQuery<NavigationData>(
    NAV_QUERY,
    {},
    { initial }
  );

  const encodeDataAttribute = useEncodeDataAttribute(
    data,
    sourceMap,
    STUDIO_ORIGIN
  );

  return (
    <nav>
      {data.map((navItem) => {
        const href = resolveHref(navItem._type, navItem.slug);
        if (!href) {
          return null;
        }
        return (
          <Link
            key={navItem._key}
            href={href}
            data-sanity={encodeDataAttribute?.([
              "navigation",
              navItem._key,
              "slug",
            ])}
          >
            {navItem.title}
          </Link>
        );
      })}
    </nav>
  );
}

Troubleshooting Stega

A number of the solutions below rely on the vercelStegaSplit function from the @vercel/stega npm package. This works for any hosting provider, not just Vercel, as they both consume the same metadata.

Install it with:

npm install @vercel/stega

Comparing field values fails

Your production front end likely evaluates values returned from the Content Lake to perform specific logic. If these values contain encoded metadata from Content Source Maps, likely, they will no longer work.

How to fix

For example, imagine a function that determines that a Sanity document's market value is the same as the current market:

function showDocument(document: SanityDocument, currentMarket: string) {
  return document.market === currentMarket
}

Without Content Source Maps, this function works as expected. However, if document.market contains encoded metadata, this comparison will fail.

If document.market is never shown on the page and will not benefit from Visual Editing, it may be best to remove it from the encoded paths in encodeSourceMapAtPath.

Alternatively, "clean" the value before comparing it. Since you'll likely do this more than once, consider extracting to a helper function.

import {vercelStegaCleanAll} from "@sanity/client/stega"

function showDocument(document: SanityDocument, currentMarket: string) {
  return vercelStegaCleanAll(document.market) === currentMarket
}

The styling of the 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>
  );
}

The wrong element is being highlighted

If the wrong element is highlighted when hovering them, it can be resolved by adding an attribute to the correct element.

How to fix

For example, if this component highlights the <h1> and you want it to highlight the entire <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/overlays, 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>

Was this article helpful?