Components
Media Popover

Media Popover

Access media-related features and options through a popover.

📸 Image

Add images by either uploading them or providing the image URL:

Customize image captions and resize images.

📺 Embed

Embed various types of content, such as videos and tweets:




Installation

npx @udecode/plate-ui@latest add media-popover

Examples

import React from 'react';
import {
  Box,
  PlateElement,
  PlateElementProps,
  Value,
} from '@udecode/plate-common';
import {
  Caption,
  CaptionTextarea,
  ELEMENT_IMAGE,
  Image,
  Resizable,
  TImageElement,
  useMediaState,
} from '@udecode/plate-media';
import { useFocused, useReadOnly, useSelected } from 'slate-react';
 
import { cn } from '@/lib/utils';
 
import { MediaPopover } from './media-popover';
 
const align = 'center';
 
export function ImageElement({
  className,
  ...props
}: PlateElementProps<Value, TImageElement>) {
  const { children, nodeProps } = props;
 
  const focused = useFocused();
  const selected = useSelected();
  const readOnly = useReadOnly();
 
  useMediaState();
 
  return (
    <MediaPopover pluginKey={ELEMENT_IMAGE}>
      <PlateElement className={cn('py-2.5', className)} {...props}>
        <figure className="group relative m-0" contentEditable={false}>
          <Resizable
            className={cn(align === 'center' && 'mx-auto')}
            options={{
              renderHandleLeft: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring after:opacity-0 after:group-hover:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant left
                    '-left-3 -ml-3 pl-3'
                  )}
                />
              ),
              renderHandleRight: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring after:opacity-0 after:group-hover:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant right
                    '-right-3 -mr-3 items-end pr-3'
                  )}
                />
              ),
              align,
              readOnly,
            }}
          >
            {/* eslint-disable-next-line jsx-a11y/alt-text */}
            <Image
              {...nodeProps}
              className={cn(
                'block w-full max-w-full cursor-pointer object-cover px-0',
                'rounded-sm',
                focused && selected && 'ring-2 ring-ring ring-offset-2',
                nodeProps?.className
              )}
            />
          </Resizable>
 
          <Caption
            className={cn('max-w-full', align === 'center' && 'mx-auto')}
          >
            <CaptionTextarea
              className={cn(
                'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
                'focus:outline-none focus:[&::placeholder]:opacity-0',
                'text-center'
              )}
              placeholder="Write a caption..."
              readOnly={readOnly}
            />
          </Caption>
        </figure>
 
        {children}
      </PlateElement>
    </MediaPopover>
  );
}
import React from 'react';
import {
  Box,
  PlateElement,
  PlateElementProps,
  Value,
} from '@udecode/plate-common';
import {
  Caption,
  CaptionTextarea,
  ELEMENT_MEDIA_EMBED,
  Resizable,
  TMediaEmbedElement,
  useMediaEmbed,
  useMediaState,
} from '@udecode/plate-media';
 
import { cn } from '@/lib/utils';
 
import { MediaPopover } from './media-popover';
 
const MediaEmbedElement = React.forwardRef<
  React.ElementRef<typeof PlateElement>,
  PlateElementProps<Value, TMediaEmbedElement>
>(({ className, children, ...props }, ref) => {
  const { focused, provider, readOnly, selected } = useMediaState();
  const { props: mediaEmbedProps, component: MediaComponent } = useMediaEmbed();
 
  return (
    <MediaPopover pluginKey={ELEMENT_MEDIA_EMBED}>
      <PlateElement
        ref={ref}
        className={cn('relative py-2.5', className)}
        {...props}
      >
        <figure
          className={cn(
            'group relative m-0 w-full',
            provider === 'twitter' &&
              '[&_.twitter-tweet]: [&_.twitter-tweet]:!mx-auto [&_.twitter-tweet]:!my-0 [&_.twitter-tweet]:p-0.5',
            provider === 'twitter' &&
              !readOnly &&
              selected &&
              '[&_.twitter-tweet]:shadow-[0_0_1px_rgb(59,130,249)]'
          )}
          contentEditable={false}
        >
          <Resizable
            className={cn('mx-auto')}
            options={{
              maxWidth: provider === 'twitter' ? 550 : '100%',
              minWidth: provider === 'twitter' ? 300 : 100,
              renderHandleLeft: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring after:opacity-0 group-hover:after:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant left
                    '-left-3 -ml-3 pl-3'
                  )}
                />
              ),
              renderHandleRight: (htmlProps) => (
                <Box
                  {...htmlProps}
                  className={cn(
                    'absolute top-0 z-10 flex h-full w-6 select-none flex-col justify-center',
                    'after:flex after:h-16 after:bg-ring  after:opacity-0 group-hover:after:opacity-100',
                    "after:w-[3px] after:rounded-[6px] after:content-['_']",
                    focused && selected && 'opacity-100',
                    // variant right
                    '-right-3 -mr-3 items-end pr-3',
                    provider === 'twitter' && '-mr-4'
                  )}
                />
              ),
            }}
          >
            <div
              className={cn(
                provider !== 'twitter' && 'pb-[56.0417%]',
                provider === 'youtube' && 'pb-[56.2061%]',
                provider === 'vimeo' && 'pb-[75%]',
                provider === 'youku' && 'pb-[56.25%]',
                provider === 'dailymotion' && 'pb-[56.0417%]',
                provider === 'coub' && 'pb-[51.25%]'
              )}
            >
              <MediaComponent
                className={cn(
                  'absolute left-0 top-0 h-full w-full rounded-sm',
                  selected && focused && 'ring-2 ring-ring ring-offset-2'
                )}
                title="embed"
                {...mediaEmbedProps}
              />
            </div>
          </Resizable>
 
          <Caption className={cn('mx-auto')}>
            <CaptionTextarea
              className={cn(
                'mt-2 w-full resize-none border-none bg-inherit p-0 font-[inherit] text-inherit',
                'focus:outline-none focus:[&::placeholder]:opacity-0',
                'text-center'
              )}
              placeholder="Write a caption..."
            />
          </Caption>
        </figure>
 
        {children}
      </PlateElement>
    </MediaPopover>
  );
});
MediaEmbedElement.displayName = 'MediaEmbedElement';
 
export { MediaEmbedElement };