Components
Resizable

Resizable

Resize nodes using handles.

Installation

npx @udecode/plate-ui@latest add resizable

Examples

📸 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:



import React from 'react';
 
import { cn, withRef } from '@udecode/cn';
import { CaptionProvider } from '@udecode/plate-caption';
import { PlateElement, withHOC } from '@udecode/plate-common';
import { ELEMENT_IMAGE, Image, useMediaState } from '@udecode/plate-media';
import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';
 
import { Caption, CaptionTextarea } from './caption';
import { MediaPopover } from './media-popover';
import {
  Resizable,
  ResizeHandle,
  mediaResizeHandleVariants,
} from './resizable';
 
export const ImageElement = withHOC(
  CaptionProvider,
  withHOC(
    ResizableProvider,
    withRef<typeof PlateElement>(
      ({ children, className, nodeProps, ...props }, ref) => {
        const {
          align = 'center',
          focused,
          readOnly,
          selected,
        } = useMediaState();
 
        const width = useResizableStore().get.width();
 
        return (
          <MediaPopover pluginKey={ELEMENT_IMAGE}>
            <PlateElement
              className={cn('py-2.5', className)}
              ref={ref}
              {...props}
            >
              <figure className="group relative m-0" contentEditable={false}>
                <Resizable
                  align={align}
                  options={{
                    align,
                    readOnly,
                  }}
                >
                  <ResizeHandle
                    className={mediaResizeHandleVariants({ direction: 'left' })}
                    options={{ direction: 'left' }}
                  />
                  <Image
                    alt=""
                    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}
                  />
                  <ResizeHandle
                    className={mediaResizeHandleVariants({
                      direction: 'right',
                    })}
                    options={{ direction: 'right' }}
                  />
                </Resizable>
 
                <Caption align={align} style={{ width }}>
                  <CaptionTextarea
                    placeholder="Write a caption..."
                    readOnly={readOnly}
                  />
                </Caption>
              </figure>
 
              {children}
            </PlateElement>
          </MediaPopover>
        );
      }
    )
  )
);
import React from 'react';
import LiteYouTubeEmbed from 'react-lite-youtube-embed';
import { Tweet } from 'react-tweet';
 
import { cn, withRef } from '@udecode/cn';
import { CaptionProvider } from '@udecode/plate-caption';
import { PlateElement, withHOC } from '@udecode/plate-common';
import {
  ELEMENT_MEDIA_EMBED,
  parseTwitterUrl,
  parseVideoUrl,
  useMediaState,
} from '@udecode/plate-media';
import { ResizableProvider, useResizableStore } from '@udecode/plate-resizable';
 
import { Caption, CaptionTextarea } from './caption';
import { MediaPopover } from './media-popover';
import {
  Resizable,
  ResizeHandle,
  mediaResizeHandleVariants,
} from './resizable';
 
export const MediaEmbedElement = withHOC(
  CaptionProvider,
  withHOC(
    ResizableProvider,
    withRef<typeof PlateElement>(({ children, className, ...props }, ref) => {
      const {
        align = 'center',
        embed,
        focused,
        isTweet,
        isVideo,
        isYoutube,
        readOnly,
        selected,
      } = useMediaState({
        urlParsers: [parseTwitterUrl, parseVideoUrl],
      });
      const width = useResizableStore().get.width();
      const provider = embed?.provider;
 
      return (
        <MediaPopover pluginKey={ELEMENT_MEDIA_EMBED}>
          <PlateElement
            className={cn('relative py-2.5', className)}
            ref={ref}
            {...props}
          >
            <figure
              className="group relative m-0 w-full"
              contentEditable={false}
            >
              <Resizable
                align={align}
                options={{
                  align,
                  maxWidth: isTweet ? 550 : '100%',
                  minWidth: isTweet ? 300 : 100,
                }}
              >
                <ResizeHandle
                  className={mediaResizeHandleVariants({ direction: 'left' })}
                  options={{ direction: 'left' }}
                />
 
                {isVideo ? (
                  isYoutube ? (
                    <LiteYouTubeEmbed
                      id={embed!.id!}
                      title="youtube"
                      wrapperClass={cn(
                        'rounded-sm',
                        focused && selected && 'ring-2 ring-ring ring-offset-2',
                        'relative block cursor-pointer bg-black bg-cover bg-center [contain:content]',
                        '[&.lyt-activated]:before:absolute [&.lyt-activated]:before:top-0 [&.lyt-activated]:before:h-[60px] [&.lyt-activated]:before:w-full [&.lyt-activated]:before:bg-top [&.lyt-activated]:before:bg-repeat-x [&.lyt-activated]:before:pb-[50px] [&.lyt-activated]:before:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',
                        '[&.lyt-activated]:before:bg-[url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAADGCAYAAAAT+OqFAAAAdklEQVQoz42QQQ7AIAgEF/T/D+kbq/RWAlnQyyazA4aoAB4FsBSA/bFjuF1EOL7VbrIrBuusmrt4ZZORfb6ehbWdnRHEIiITaEUKa5EJqUakRSaEYBJSCY2dEstQY7AuxahwXFrvZmWl2rh4JZ07z9dLtesfNj5q0FU3A5ObbwAAAABJRU5ErkJggg==)]',
                        'after:block after:pb-[var(--aspect-ratio)] after:content-[""]',
                        '[&_>_iframe]:absolute [&_>_iframe]:left-0 [&_>_iframe]:top-0 [&_>_iframe]:size-full',
                        '[&_>_.lty-playbtn]:z-[1] [&_>_.lty-playbtn]:h-[46px] [&_>_.lty-playbtn]:w-[70px] [&_>_.lty-playbtn]:rounded-[14%] [&_>_.lty-playbtn]:bg-[#212121] [&_>_.lty-playbtn]:opacity-80 [&_>_.lty-playbtn]:[transition:all_0.2s_cubic-bezier(0,_0,_0.2,_1)]',
                        '[&:hover_>_.lty-playbtn]:bg-[red] [&:hover_>_.lty-playbtn]:opacity-100',
                        '[&_>_.lty-playbtn]:before:border-y-[11px] [&_>_.lty-playbtn]:before:border-l-[19px] [&_>_.lty-playbtn]:before:border-r-0 [&_>_.lty-playbtn]:before:border-[transparent_transparent_transparent_#fff] [&_>_.lty-playbtn]:before:content-[""]',
                        '[&_>_.lty-playbtn]:absolute [&_>_.lty-playbtn]:left-1/2 [&_>_.lty-playbtn]:top-1/2 [&_>_.lty-playbtn]:[transform:translate3d(-50%,-50%,0)]',
                        '[&_>_.lty-playbtn]:before:absolute [&_>_.lty-playbtn]:before:left-1/2 [&_>_.lty-playbtn]:before:top-1/2 [&_>_.lty-playbtn]:before:[transform:translate3d(-50%,-50%,0)]',
                        '[&.lyt-activated]:cursor-[unset]',
                        '[&.lyt-activated]:before:pointer-events-none [&.lyt-activated]:before:opacity-0',
                        '[&.lyt-activated_>_.lty-playbtn]:pointer-events-none [&.lyt-activated_>_.lty-playbtn]:!opacity-0'
                      )}
                    />
                  ) : (
                    <div
                      className={cn(
                        provider === 'vimeo' && 'pb-[75%]',
                        provider === 'youku' && 'pb-[56.25%]',
                        provider === 'dailymotion' && 'pb-[56.0417%]',
                        provider === 'coub' && 'pb-[51.25%]'
                      )}
                    >
                      <iframe
                        allowFullScreen
                        className={cn(
                          'absolute left-0 top-0 size-full rounded-sm',
                          isVideo && 'border-0',
                          focused &&
                            selected &&
                            'ring-2 ring-ring ring-offset-2'
                        )}
                        src={embed!.url}
                        title="embed"
                      />
                    </div>
                  )
                ) : null}
 
                {isTweet && (
                  <div
                    className={cn(
                      '[&_.react-tweet-theme]:my-0',
                      !readOnly &&
                        selected &&
                        '[&_.react-tweet-theme]:ring-2 [&_.react-tweet-theme]:ring-ring [&_.react-tweet-theme]:ring-offset-2'
                    )}
                  >
                    <Tweet id={embed!.id!} />
                  </div>
                )}
 
                <ResizeHandle
                  className={mediaResizeHandleVariants({ direction: 'right' })}
                  options={{ direction: 'right' }}
                />
              </Resizable>
 
              <Caption align={align} style={{ width }}>
                <CaptionTextarea placeholder="Write a caption..." />
              </Caption>
            </figure>
 
            {children}
          </PlateElement>
        </MediaPopover>
      );
    })
  )
);