← Swarmify Blog

React Video Player Comparison: Tested Options (2026)

Compare tested React video players, measured production bundle sizes, HLS playback, captions, keyboard controls, and Next.js integration limits.

James Christensen · November 9, 2026 · 18 min read
Watercolor illustration of a video source branching into streams and a managed delivery path

You can add video to a React app with the native <video> element or a player library that manages playback. Choose a React video player based on your sources and which delivery work your team wants to own. Start with native video for short MP4s, hls.js for prepared HLS streams, ReactPlayer for mixed providers, or Video.js 10’s React components for a themed player. A hosted service can also take over encoding and delivery.

In our testing on October 22, 2026, each option was installed in a scratch React app and built against an empty-app baseline. Installing video.js no longer gave us the imperative player used in older React tutorials: its version 10 package threw a migration error, while the current React integration used @videojs/react.

The measurements below use React 19.3.1, Vite 8.3.5, Node 22.22.2, and Linux. They measure emitted production JavaScript plus CSS, minified and gzip-compressed, not video bytes or startup speed. Documentation was checked on the same date; parenthetical citation years are the year checked, except the dated React 19 release notes.

📋 TL;DR • Simple MP4: Native <video> adds almost no JavaScript. A single MP4 has one encoded quality; native HLS is a separate browser capability. • Streaming: hls.js adds adaptive HLS playback to a normal video element; you mount it in useEffect and destroy it on unmount. • Mixed sources: ReactPlayer v3 plays files, HLS, DASH, YouTube, Vimeo, Wistia, and Mux from one component, and is now maintained by Mux. • No pipeline: A hosted player takes encoding, adaptive delivery, and CDN configuration off your team’s hands, in exchange for using that host.

Which React video player options fit each use case?

The native element adds essentially no library code; streaming engines and themed controls add different amounts. Here is the incremental bundle contribution over the same empty React app:

Tested integrationInstalled versionMinified contributionGzipped contribution
Native <video> + caption trackReact 19.3.1182 B110 B
hls.js + native controls1.7.3575,764 B176,455 B (172.3 KiB)
ReactPlayer, default multiprovider import3.4.02,050,937 B592,816 B (578.9 KiB)
Video.js 8 wrapper + CSS8.24.2749,264 B215,356 B (210.3 KiB)
Video.js 10 React skin + hls.js adapter10.2.0903,113 B248,539 B (242.7 KiB)
Plyr React + CSS, without HLS engineplyr-react 6.0.0 / Plyr 3.8.5146,629 B38,264 B (37.4 KiB)
Plyr React + hls.js + CSSSame, plus hls.js 1.7.3722,090 B214,534 B (209.5 KiB)
Vidstack + default video layout/CSS@vidstack/react 1.15.7423,812 B125,502 B (122.6 KiB)
Mux Player React3.14.01,114,392 B313,203 B (305.9 KiB)

Each row is a separate Vite production build minus the baseline’s 219,524 minified bytes / 67,990 gzipped bytes. Gzip level 9 was applied to each emitted JS/CSS file separately, then summed. ReactPlayer’s number includes all emitted deferred provider chunks, not just the chunks an HLS visit downloads. Vidstack’s total excludes its externally fetched HLS engine. Those are build-output comparisons, not equivalent first-load transfer measurements. SmartVideo’s externally loaded assets are measured separately below; counting only its React wrapper would misrepresent the player’s cost.

Library adoption can help assess the size of a user community, though downloads and stars are not measures of quality or maintenance. This snapshot covers npm downloads for November 2–8, 2026 and GitHub stars checked November 9; Mux’s repository contains several components.

Projectnpm downloads that weekRepository stars
hls.js14,733,15417,042
ReactPlayer3,009,12110,290
video.js1,590,13140,141
plyr-react82,853—
@vidstack/react680,909—
@mux/mux-player-react3,234,775—

For an owned website, separate the player decision from the hosting decision. None of these npm packages creates an HLS quality ladder from an upload. A managed service can supply that pipeline, but installing its player is not the same as provisioning hosted video.

Watercolor illustration of a file source, HLS quality ladder, and hosted delivery path feeding one player frame The native element, streaming library, and hosted player each cover a different part of playback. Conceptual illustration, not a software interface.

What happened with HLS, captions, and keyboard controls?

The tested players generally played the HLS sample, but caption and keyboard behavior varied.

The playback test used the public Mux sample at https://test-streams.mux.dev/x36xhzz/x36xhzz.m3u8, with a locally served one-cue WebVTT file. Headless Chromium 156.0.8078.4 opened only the local test app. A resolved play() call plus an advancing currentTime counted as playback; a recognized filename alone did not.

We noticed native HLS worked in this Chromium build: canPlayType('application/vnd.apple.mpegurl') returned "maybe", and the plain <video> actually played the sample. Plyr without hls.js also used that native path. That does not establish native HLS support across Chrome installations, Firefox, Safari, or mobile devices. Keep capability detection and test your real browser matrix; do not assume either “all Chromium needs hls.js” or “no Chromium needs it.”

The hls.js component, ReactPlayer, Video.js 8, Video.js 10 with its hls.js adapter, Plyr with an attached hls.js instance, Vidstack, and Mux Player also advanced through the stream. Space and Right Arrow worked with the native controls, ReactPlayer, Plyr, Mux, and v10. Vidstack responded when its player container had focus, not when the test focused its internal video. The v8 play button responded to Space. In a separate paused-video check, Right Arrow left currentTime at 20 seconds with either the .video-js player or its play button focused; the tested default v8 configuration did not seek in either case.

The local WebVTT cue loaded through native video, hls.js, ReactPlayer, Plyr’s native path, and Mux Player. Video.js 8 exposed a loaded cue through its emulated textTracks() API. The v10 skin initially left the caption track disabled despite the fixture’s default attribute; pressing c while a player control had focus loaded the cue and switched it to showing. Vidstack rendered the cue in its own caption layer. Test caption selection and visible output, not just the existence of a <track> tag.

Watercolor illustration of a video player with captions and keyboard keys Playback, captions, and keyboard controls need separate checks in each player.

The Plyr/hls.js fixture initially had no text tracks after attachment. Appending a WebVTT track afterward loaded a cue through the video element, but it did not populate Plyr’s caption menu in that fixture. That integration needs more caption-lifecycle work before shipping; successful video playback was not a full captions pass.

The cross-library checks use one public HLS sample; SmartVideo additionally uses its existing public demo asset. One local caption file was used throughout. These tests do not establish DRM, live latency, screen-reader accessibility, mobile behavior, or hosted upload performance.

How does the native <video> element work in React?

A native <video> element is enough when a React component needs to play a single MP4 file.

export function ProductDemo() {
  return (
    <video
      src="/media/product-demo.mp4"
      poster="/media/product-demo-poster.jpg"
      width={1280}
      height={720}
      controls
      playsInline
      preload="metadata"
      style={{ width: '100%', height: 'auto' }}
    />
  );
}

Note the JSX spellings: playsInline, autoPlay, and className render as the HTML attributes playsinline, autoplay, and class. According to MDN, explicit dimensions help avoid layout shifts, while preload="metadata" is only a browser hint, not a download limit (MDN video reference, 2026). The browser can ignore it, and autoplay takes precedence.

For a muted background loop, add autoPlay muted loop playsInline, but do not assume playback started. According to MDN, HTMLMediaElement.play() returns a Promise that can reject with NotAllowedError when autoplay policy blocks script-started playback (MDN play() reference, 2026). Update a custom “playing” state only after that Promise resolves, and keep a user-initiated play option.

Where it breaks down: a single MP4 cannot switch to another encoded rendition as network conditions change. Our guide to the HTML5 video player covers the element’s attributes in more depth; the rest of this post is about what to add when one file is not enough.

How does hls.js add adaptive streaming to React?

hls.js lets a React component play a prepared HLS stream in a video element, while your stack still handles packaging and delivery. This is adaptive bitrate streaming; the player switches among prepared renditions as conditions change.

According to the hls.js README, the library relies on HTML5 video and Media Source Extensions for playback; it does not encode an upload (hls.js README, 2026). The DASH vs. HLS comparison covers the protocol trade-offs. In React, the integration needs a ref for the element and an effect for the library’s lifecycle:

'use client';
import { useEffect, useRef } from 'react';
import Hls from 'hls.js';

export function HlsVideo({ src, poster }) {
  const videoRef = useRef(null);

  useEffect(() => {
    const video = videoRef.current;
    if (!video) return;

    if (Hls.isSupported()) {
      const hls = new Hls();
      hls.loadSource(src);
      hls.attachMedia(video);
      return () => hls.destroy();
    }

    if (video.canPlayType('application/vnd.apple.mpegurl')) {
      video.src = src;
    }
  }, [src]);

  return (
    <video
      ref={videoRef}
      poster={poster}
      controls
      playsInline
      style={{ width: '100%', height: 'auto' }}
    />
  );
}

Two details are worth copying exactly. First, the cleanup function destroys the hls.js instance when the effect is cleaned up. Second, the order of the checks follows the hls.js README’s recommended default: try Hls.isSupported() before native playback. Safari has built-in HLS, but other browsers can report a non-empty canPlayType() result without reliably playing every HLS stream.

According to Next.js, 'use client' declares a boundary for interactive components that need state, event handling, or browser APIs (Next.js documentation, 2026). In the App Router, put it on the player entry module imported by a Server Component; you do not need it in every module below that boundary.

The tested hls.js 1.7.3 component added 172.3 KiB gzipped with browser-native controls. Its WebVTT cue loaded and its keyboard controls worked because it still used the underlying video element; hls.js did not supply a themed control bar.

What hls.js does not do: it plays an .m3u8 playlist. It does not create one. You still need to transcode each upload into a quality ladder, package the segments, store them, and serve them from a CDN. Our HLS streaming guide and m3u8 player explainer walk through that side.

Watercolor illustration of an HLS stream connected to a video element with a detachable link The React effect attaches the stream handler and cleans it up when the source changes. Conceptual illustration, not a code screenshot.

When should you use ReactPlayer for mixed video sources?

ReactPlayer suits apps that accept video URLs from several providers rather than controlling every file themselves.

According to its README, supported sources include file paths, HLS, DASH, YouTube, Vimeo, Wistia, and Mux (ReactPlayer README, 2026). It selects the appropriate playback engine or provider SDK for the URL; it does not turn that URL into a separately hosted asset.

import ReactPlayer from 'react-player';

export function LessonVideo({ url }) {
  return <ReactPlayer src={url} controls />;
}

Two current facts change how you should evaluate it:

  • Version 3 is a breaking rewrite. The README says v3 “is not backwards compatible with v2.” Older tutorials that pass a url prop were written for v2; v3 examples use src.
  • Mux now maintains it. The README announces that maintenance “is being taken over by Mux,” with the project remaining open source.

ReactPlayer fits a course builder or CMS where authors paste URLs from different platforms. If you control every source file, compare it with a native element or streaming-specific library before adding a multiprovider abstraction. A shared component does not remove a provider’s branding or embed behavior; see why YouTube embeds hurt your website for the owned-site trade-off.

The README also warns that ReactPlayer “is not guaranteed to function properly on mobile devices” because of platform restrictions. Test the source types your app accepts on real phones, including the first user-initiated play and any autoplay behavior. Treat a supported URL pattern as a starting point, not proof that a private or restricted video will play.

ReactPlayer 3.4.0 played the HLS sample without a separately wired hls.js effect, loaded the child WebVTT track, and exposed working native keyboard controls. Its 578.9 KiB gzipped build contribution is the full emitted multiprovider output. The ReactPlayer README explains that compatible bundlers lazy-load the appropriate player, so this number should not be presented as every viewer’s initial download.

Watercolor illustration of one React component routing video sources to different playback engines A shared component can choose a playback path based on the source type. Conceptual illustration, not a software interface.

Which Video.js integration should a React app use?

Video.js 10 uses React components; Video.js 8 uses the older videojs() initialization-and-disposal pattern. They are not interchangeable install instructions.

The Video.js migration guide explicitly says the video.js npm package now contains no player. Our install resolved to 10.2.0. Calling videojs() produced VJS8_LEGACY_INIT, not a playable video. Pin video.js@8 if you deliberately keep an existing v8 integration; otherwise install the current React package and its chosen media adapter:

npm install @videojs/react @videojs/hlsjs-video
'use client';
import '@videojs/react/video/skin.css';
import { VideoPlayer, VideoSkin } from '@videojs/react/video';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';

export function StreamVideo({ src }) {
  return (
    <VideoPlayer>
      <VideoSkin>
        <HlsJsVideo src={src} playsInline>
          <track
            src="/captions.vtt"
            kind="captions"
            srcLang="en"
            label="English"
            default
          />
        </HlsJsVideo>
      </VideoSkin>
    </VideoPlayer>
  );
}

The tested 10.2.0 skin plus hls.js adapter added 242.7 KiB gzipped. The separate adapter package is required: installing @videojs/react alone left the HLS adapter import unresolved in the build. The skin supplies controls; the media component supplies playback. According to the migration guide, the React player owns its lifecycle, so this integration does not use a manual dispose() effect.

For comparison, the tested v8 wrapper added 210.3 KiB gzipped, including CSS and its streaming engine. That wrapper still creates a video-js element inside a stable React-owned container, initializes once in an effect, and calls dispose() during cleanup. Do not move v8 plugins into a v10 app without checking the migration guide’s known gaps.

For an existing v8 application, this wrapper creates the player inside a React-owned container and disposes it on unmount:

import { useEffect, useRef } from 'react';
import videojs from 'video.js';
import 'video.js/dist/video-js.css';

export function LegacyVideo({ src }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const element = document.createElement('video-js');
    containerRef.current.appendChild(element);
    const player = videojs(element, { controls: true, sources: [{ src }] });
    return () => player.dispose();
  }, [src]);

  return <div ref={containerRef} />;
}

Are Plyr React and Vidstack good alternatives for new apps?

Plyr React and Vidstack are worth recognizing in existing codebases, but their measured size is not enough to recommend them for a new app.

Installing Plyr 3.8.5 printed a deprecation warning. Its npm metadata says it receives security updates only and recommends Video.js 10. The Vidstack 1.15.7 metadata likewise marks it deprecated, with security fixes only until January 2028.

The small Plyr-only result, 37.4 KiB gzipped, excludes an HLS engine. Adding hls.js raised it to 209.5 KiB. The wrapper’s current API also differs from older tutorials: the installed plyr-react 6.0.0 exports a named Plyr, and its stylesheet path is plyr-react/plyr.css. Check the installed exports rather than copying an older default import.

Strict TypeScript checking with TypeScript 7.0.2 exposed two library-declaration errors in that Plyr pairing: missing Options and SourceInfo exports. Vidstack initially needed @types/trusted-types; even after installing it, its declarations reported that dashjs had no default export. These are findings for the tested dependency tree, not claims that every TypeScript setup fails. The other working player fixtures passed the corrected Vite type check without disabling library checks.

Vidstack’s 122.6 KiB gzipped build used its default video layout. At runtime it separately fetched hls.js 1.7.3 from jsDelivr; the fetched engine is not in that build number. Its HLS sample and visible WebVTT caption worked. Space and Right Arrow worked with focus on the player container. This is why comparing npm bundle totals alone can make an externally loaded engine look artificially cheap.

Does 'use client' prevent SSR and hydration errors?

A Client Component boundary does not prevent server rendering or guarantee hydration without errors.

According to Next.js’s lazy-loading guide, Client Components are prerendered by default; use next/dynamic with ssr: false inside a Client Component when a browser-only dependency must skip that step.

In our testing with Next.js 16.4.2’s development App Router, native video, the hls.js component, Vidstack, and the v10 component returned HTTP 200 with a video element in the server HTML. The v8 and SmartVideo effect-based wrappers returned empty containers and created their children after hydration. ReactPlayer and Mux also returned HTTP 200 without an observed hydration-mismatch exception; their browser media can live inside custom-element shadow roots rather than appear in a document-level video query.

Plyr React was different: importing the tested component in a 'use client' route still returned HTTP 500, document is not defined, during server rendering. Dynamically importing that fixture with ssr: false from a Client Component changed the response to HTTP 200 and mounted a video in the browser. External requests were deliberately blocked during these hydration checks, so resulting media/resource errors are not evidence that those players failed playback. This was a development SSR check, not a production Next.js build or a complete route-transition test.

Watercolor illustration of a player control joining a browser video frame Server rendering and browser initialization are separate stages of a React integration.

When should you use a hosted player with React?

A managed video service can handle encoding, delivery, and playback for a React website.

The player alone is not that service: check which hosting product supplies its sources and which upload workflow you still need to build. Two approaches illustrate the difference for an owned React website.

Mux Player React

Choose Mux Player when you want a React component tied to Mux-hosted video. Adding the package alone does not upload or process your original files; plan that asset workflow separately.

The Mux Player 3.14.0 fixture added 305.9 KiB gzipped and played the public test stream via src, with a loaded caption cue and working Space/Right Arrow controls. This tested the component without uploading anything or provisioning a Mux asset; it did not test the hosted encoding workflow.

SmartVideo’s framework-agnostic embed

SmartVideo uses a dashboard snippet rather than an npm player package. Its framework guide says React should render an empty wrapper, create the smartvideo div after mount, and dispose an initialized player before removing its DOM. Load the snippet once outside route components, set embed attributes before insertion, and let SmartVideo own the wrapper’s children.

The local fixture used the existing public asset and public demo configuration from SmartVideo’s demo; no account was created and no video was uploaded. Playback advanced, including with an external HLS URL, the one-cue WebVTT caption appeared, and Space on the play button resumed playback. A paused-video retest isolated keyboard focus: Right Arrow moved currentTime from 20 to 25 seconds with the .video-js player focused, but left it at 20 seconds with the play button focused. The shortcut works on the player, not while that control button has focus. The wrapper rendered empty on the server and inserted its embed after hydration.

Its React wrapper added 1,065 minified bytes / 529 gzipped bytes, not the player cost. SmartVideo’s one-script embed loads the whole hosted player. Fetched JS/CSS totaled 293,129 gzipped bytes (286.3 KiB), including the loader, CSS, configuration, and 10,480 gzipped bytes of YouTube iframe-API code loaded by the demo. Uncompressed responses totaled 1,104,081 bytes. Each response was gzipped separately at level 9; media and fonts are excluded. External response totals are not directly comparable to the npm build output above. The loader is unversioned; the October 22 capture’s SHA-256 begins 99c7980bdab7.

The guide also documents a cleanup limitation: disposing an initialized player does not cancel initialization still awaiting a network request. On iOS and other native-HLS paths, unmounting during that wait can leave a player undisposed. The test did not exercise that race or validate source changes.

According to SmartVideo’s pricing page, storage, encoding, and CDN delivery are included, with view allowances and overages defining usage billing. Its player feature page describes multiple encodes, brand colors, configurable controls, and no suggested-video recommendations. Those managed-service capabilities were not measured by the player fixture.

Building video for your own site? If you need branded playback but would rather not maintain transcoding, storage, and a CDN, see how the SmartVideo player works. Use the framework guide above to evaluate React lifecycle requirements before committing to the embed.

How should you choose a React video player?

Choose a React video player by source, content length, customization needs, and ownership of encoding and delivery.

  1. Do you control the source files? If not, and users paste links from YouTube, Vimeo, or Wistia, use ReactPlayer.
  2. Is it a short clip or a background loop? The native <video> tag is enough. Compress it well and set preload sensibly.
  3. Is it long-form or important content, like demos, lessons, or onboarding? Consider adaptive streaming. Decide whether you want to build that pipeline (hls.js or Video.js plus your own transcoding and CDN) or rent it (a managed video service).
  4. Do you need deep UI customization? Compare the current Video.js React skins with a custom UI on hls.js. If upgrading an existing v8 app, check plugin compatibility rather than assuming its imperative API survives.
  5. Do you need viewer data? Plan the collection layer now; a native element does not send events to your reporting system by itself. See our overview of video analytics for metrics worth tracking.

If you are still comparing complete platforms rather than React components, our website video player roundup compares hosted players side by side.

What should you know about React video players?

React video player choices depend on source formats, browser behavior, frameworks, and hosting needs.

Which video player should you use with React?

No single option fits every app. The native <video> element handles a simple MP4, hls.js fits your own adaptive streams, ReactPlayer works when URLs come from several platforms, and Video.js fits deeper customization. A hosted player is useful when you do not want to run encoding and a CDN yourself.

Does React have a built-in video player?

No. React renders the browser’s native HTML5 <video> element but ships no player component of its own. Native video includes controls and may support HLS streaming directly, as the tested Chromium build did. Other browsers or source formats can need a streaming library; custom skins and reporting still need application or vendor code.

Is ReactPlayer still maintained?

Yes. Its README states that maintenance is being taken over by Mux and that the project remains open source. The tested package was ReactPlayer 3.4.0. Version 3 is not backwards compatible with v2, so check the migration guide and the source providers your app actually accepts.

How do I play an HLS (.m3u8) stream in React?

Create a component with a ref to a <video> element, then in useEffect attach hls.js when Hls.isSupported() is true and fall back to setting video.src directly when the browser has native HLS support. Destroy the hls.js instance in the effect’s cleanup so the stream stops when the component unmounts.

Why won’t my React video autoplay?

Browsers can block autoplay with sound and reject the play() Promise with NotAllowedError. Use muted, autoPlay, and playsInline for a muted inline attempt, but keep a visible play option. These attributes do not override browser or user policy, so reflect the actual playback result in your controls.

Can I use a video player in Next.js server components?

Interactive players generally need a Client Component boundary; a plain native video element does not. Mark the interactive entry module with 'use client', but remember Next.js can still prerender it. The tested Plyr React import failed on the server until it was dynamically loaded with ssr: false from a Client Component. Other fixtures behaved differently, so test the actual library and version.

Do I need a video host if I use a React video player?

You need somewhere to store and serve the files either way. For a few short clips, your own storage and CDN may be enough. For longer or business-critical video, a host that transcodes into multiple quality levels and serves them from a CDN removes most of the work a React player cannot do for you.

Can I use web components or custom elements for video in React?

Yes. According to the React 19 release notes, client rendering assigns matching custom-element properties as properties and other props as attributes (React 19 release notes, 2024). Server rendering follows different rules: supported primitive values become attributes, while non-primitive values are omitted. Check your player’s integration guide rather than assuming browser-only examples behave identically during server rendering.

What should you take away when comparing React video players?

Pick the React video player that covers playback without hiding infrastructure work your team still owns.

ReactPlayer and Video.js add source or skin flexibility but do not replace that pipeline. A hosted player trades a monthly bill for not running it. If your team’s time is better spent on the product than on transcoding jobs and CDN configuration, explore SmartVideo for your React app and review its framework integration guide.

Try SmartVideo in Your React App →

Keep Visitors on Your Page, Not YouTube’s

SmartVideo’s player is 2x lighter than a YouTube embed, with zero ads. No suggested videos, no one else’s logo.

Build your player
10% conversion increase
“We’ve had a 10% increase in conversion rate from lead to customer.”
Pedro C.
Dir. of Digital Marketing, White Jacobs
Instant side-by-side winner
“SmartVideo loaded immediately; YouTube took 5 seconds to start. It became a no-brainer.”
Aaron H.
IT Director, PizzaTV
Rankings improved
“Our search-engine rankings went up after we switched over to Swarmify.”
Philip A.
Owner, Ignite Images