Skip to content

Video

The Video extension allows you to add a video to your editor.

Usage

tsx
import { RichTextProvider } from 'reactjs-tiptap-editor'

// Base Kit
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Dropcursor, Gapcursor, Placeholder, TrailingNode } from '@tiptap/extensions'
import { HardBreak } from '@tiptap/extension-hard-break'
import { TextStyle } from '@tiptap/extension-text-style';
import { ListItem } from '@tiptap/extension-list';

// Extension
import { Video, RichTextVideo } from 'reactjs-tiptap-editor/video'; 
// ... other extensions


// Import CSS
import 'reactjs-tiptap-editor/style.css';

const extensions = [
  // Base Extensions
  Document,
  Text,
  Dropcursor,
  Gapcursor,
  HardBreak,
  Paragraph,
  TrailingNode,
  ListItem,
  TextStyle,
  Placeholder.configure({
    placeholder: 'Press \'/\' for commands',
  })

  ...
  // Import Extensions Here
  Video.configure({
    resourceVideo: 'both',
    acceptMimes: ['video/mp4', 'video/webm'],
    maxSize: 100 * 1024 * 1024,
    multiple: true,
    uploadConcurrency: 3,
    showUploadProgress: true,
    upload: (file, { onProgress } = {}) => uploadVideo(file, onProgress),
    onError: ({ message, file }) => {
      console.error(message, file?.name);
    },
  })
];

const RichTextToolbar = () => {
  return (
    <RichTextVideo /> {}
  )
}

const App = () => {
   const editor = useEditor({
    textDirection: 'auto', // global text direction
    extensions,
  });

  return (
    <RichTextProvider
      editor={editor}
    >
      <RichTextToolbar />

      <EditorContent
        editor={editor}
      />
    </RichTextProvider>
  );
};

fetch does not expose upload byte progress. Use XMLHttpRequest, Axios, or a storage SDK that reports transferred bytes when you need a real progress bar:

ts
function uploadVideo(
  file: File,
  onProgress?: (progress: { loaded: number; total: number }) => void
) {
  return new Promise<string>((resolve, reject) => {
    const formData = new FormData();
    formData.append('file', file);

    const request = new XMLHttpRequest();
    request.open('POST', '/api/videos');
    request.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        onProgress?.({ loaded: event.loaded, total: event.total });
      }
    });
    request.addEventListener('load', () => {
      if (request.status < 200 || request.status >= 300) {
        reject(new Error('Video upload failed'));
        return;
      }

      resolve(JSON.parse(request.responseText).url);
    });
    request.addEventListener('error', () => reject(new Error('Video upload failed')));
    request.send(formData);
  });
}

Props

ts
interface VideoUploadProgress {
  loaded: number;
  total: number;
}

interface VideoUploadContext {
  onProgress?: (progress: VideoUploadProgress) => void;
}

interface VideoOptions extends GeneralOptions<VideoOptions> {
  /**
   * Indicates whether fullscreen play is allowed
   *
   * @default true
   */
  allowFullscreen: boolean;
  /**
   * Indicates whether to display the frameborder
   *
   * @default false
   */
  frameborder: boolean;
  /**
   * Width of the video, can be a number or string
   *
   * @default VIDEO_SIZE['size-medium']
   */
  width: number | string;
  /** HTML attributes object for passing additional attributes */
  HTMLAttributes: {
    [key: string]: any;
  };
  /** Function for uploading files */
  upload?: (file: File, context?: VideoUploadContext) => Promise<string>;

  /** Whether multiple videos can be selected and uploaded at once */
  multiple?: boolean;

  /** Maximum number of videos uploaded concurrently */
  uploadConcurrency?: number;

  /**
   * Whether to display overall and per-file upload progress
   *
   * @default true
   */
  showUploadProgress?: boolean;

  /** Accepted video MIME types or file extensions */
  acceptMimes?: string[];

  /** Maximum size of a single video in bytes. No limit is applied when omitted. */
  maxSize?: number;

  /** Callback invoked when video validation or upload fails */
  onError?: (error: { type: 'size' | 'type' | 'upload'; message: string; file?: File }) => void;

  /** The source URL of the video */
  resourceVideo: 'upload' | 'link' | 'both';

  /**
   * List of allowed video hosting providers.
   * Use ['.'] to allow any URL.
   *
   * @default ['.']
   */
  videoProviders?: string[];
}

Options

OptionTypeDescriptionRequiredDefault
allowFullscreenbooleanAllows embedded videos to enter fullscreen mode.Notrue
frameborderbooleanDisplays a border around the embedded video frame.Nofalse
widthnumber | stringSets the default video width.NoVIDEO_SIZE.size-medium
HTMLAttributesRecord<string, any>Adds HTML attributes to the video wrapper.No{ class: 'iframe-wrapper' }
upload(file: File, context?: VideoUploadContext) => Promise<string>Uploads a local video, optionally reports byte progress, and resolves with its URL.NoNone
multiplebooleanAllows selecting and uploading multiple videos.Notrue
uploadConcurrencynumberLimits the number of videos uploaded at the same time. Values below 1 are treated as 1.No3
showUploadProgressbooleanDisplays overall and per-file progress when byte progress is reported.Notrue
acceptMimesstring[]Restricts local files by MIME type or extension; wildcard values such as video/* are supported.No['video/*']
maxSizenumberMaximum size of each local video in bytes. No size limit is applied when omitted.NoNone
onError(error: { type: 'size' | 'type' | 'upload'; message: string; file?: File }) => voidHandles validation and upload failures. When omitted, the editor displays its default error toast.NoNone
resourceVideo'upload' | 'link' | 'both'Controls whether users can add videos by local upload, URL, or both.No'both'
videoProvidersstring[]Restricts linked videos to matching providers. Use ['.'] to accept any URL.No['.']

Upload behavior

While the upload promise is pending, both the toolbar dialog and the slash-command dialog stay open and disable the upload button. Call context.onProgress({ loaded, total }) to show real, byte-weighted total progress and per-file progress. If an existing upload function ignores the optional second argument, it remains compatible and the editor shows an indeterminate spinner. Progress details are enabled by default. Set showUploadProgress: false to hide the overall and per-file progress UI while keeping the disabled upload button and indeterminate loading indicator.

When all uploads resolve, their URLs are inserted in selection order and the dialog closes. A file that reaches 100% before its upload promise resolves is shown as processing.

If one upload rejects, other successful videos are still inserted. The failed file remains marked in the open dialog so the user can select it again. Configure onError to provide custom error handling; otherwise, the editor shows its default upload error toast.

acceptMimes and maxSize validate every selected file before uploading. With multiple: true, all valid files are queued and up to uploadConcurrency files upload at once.

Source

SourceDocs

Contributors

Changelog

v1.0.0 on 12/7/2025
51948 - feat: refactor code, dynamic bubble, toolbar, fix error
v0.3.5 on 6/19/2025
10881 - fixed vimeo video links embedding + youtube shorts
a7323 - feat(video): add alignment options for video elements
v0.3.3 on 5/13/2025
2cc0b - fix(active video button): added videoProviders prop + validation

Made with ❤️