Custom YouTube preview component not working in Sanity v3

3 replies
Last updated: Jan 21, 2026
Hello everyone! πŸ™‚
Im trying to make a custom preview of a embed youtube video but it doesnt work. What am i doing wrong? Its version 3.


import { defineField, defineType } from "sanity";
import getYouTubeId from "get-youtube-id";

interface Youtube {
  _type: "youtube";
  url: string;
}

interface PreviewProps {
  media: React.ReactNode;
  title: string;
}

const YoutubePreview: React.FC<PreviewProps> = ({ media, title }) => {
  const id = getYouTubeId(media as string);
  const embedUrl = `<https://www.youtube.com/embed/${id}>`;
  return (
    <div>
      <h2>{title}</h2>
      <iframe
        width="560"
        height="315"
        src={embedUrl}
        title="YouTube video player"
        frameBorder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
      ></iframe>
    </div>
  );
};

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  preview: {
    select: {
      url: "url",
    },
    prepare(selection) {
      const { url } = selection;
      if (!url) {
        return {
          title: "Missing YouTube URL",
          media: <div>Missing YouTube URL</div>,
        };
      }
      const id = getYouTubeId(url);
      const media = (
        <img
          src={`<https://i.ytimg.com/vi/${id}/hqdefault.jpg>`}
          alt="Video thumbnail"
        />
      );
      return {
        title: "YouTube Video",
        media,
      };
    },
    component: YoutubePreview,
  },
}) as unknown as Youtube;
AI Update

I can see the issue! In Sanity v3, the way you define custom preview components has changed. You're mixing the old component property with the preview.prepare() method, which doesn't work together properly.

The Problem:

In v3, when you want a custom preview component, you need to use components.preview (not preview.component). Also, when you define a custom preview component, it receives specific props from the Form Components API, not the props you return from prepare().

The Solution:

You have two options depending on what you want to achieve:

Option 1: Simple thumbnail preview (recommended for most cases)

If you just want to show the YouTube thumbnail in lists and references, stick with the standard preview configuration without a custom component:

import { defineType } from "sanity";
import getYouTubeId from "get-youtube-id";

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  preview: {
    select: {
      url: "url",
    },
    prepare(selection) {
      const { url } = selection;
      if (!url) {
        return {
          title: "Missing YouTube URL",
        };
      }
      const id = getYouTubeId(url);
      return {
        title: "YouTube Video",
        subtitle: url,
        media: id ? (
          <img
            src={`https://i.ytimg.com/vi/${id}/hqdefault.jpg`}
            alt="Video thumbnail"
          />
        ) : undefined,
      };
    },
  },
});

Option 2: Full custom preview component with embedded video

If you want the actual embedded video player to show in the preview, use components.preview. Note that the custom component receives different props than what prepare() returns - it gets the entire document value:

import { defineType } from "sanity";
import getYouTubeId from "get-youtube-id";
import { PreviewProps } from "sanity";

const YoutubePreview = (props: PreviewProps) => {
  const { title } = props;
  const url = (props as any).value?.url; // Access the actual field value
  
  if (!url) {
    return <div>Missing YouTube URL</div>;
  }
  
  const id = getYouTubeId(url);
  const embedUrl = `https://www.youtube.com/embed/${id}`;
  
  return (
    <div style={{ padding: '1em' }}>
      <h3>{title || 'YouTube Video'}</h3>
      <iframe
        width="100%"
        height="315"
        src={embedUrl}
        title="YouTube video player"
        frameBorder="0"
        allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
        allowFullScreen
      />
    </div>
  );
};

export default defineType({
  name: "youtube",
  type: "object",
  fields: [
    {
      name: "url",
      title: "URL",
      type: "url",
      validation: (Rule) => Rule.required(),
    },
  ],
  components: {
    preview: YoutubePreview  // Use components.preview, not preview.component
  },
  preview: {
    select: {
      url: "url",
    },
  },
});

Key differences in v3:

  1. Use components.preview instead of preview.component (the old v2 way)
  2. Custom preview components receive PreviewProps which includes the document value, not the result of prepare()
  3. The prepare() function is still useful for standard previews (Option 1), but isn't used when you have a custom component

Option 1 is usually better for performance since it just shows a thumbnail, while Option 2 embeds the full video player which might slow down your Studio if you have many videos in a list.

Hope this helps! πŸŽ₯

Show original thread
3 replies

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?