# Add a quality selector

Read available renditions, let the engine adapt automatically, and offer manual quality selection.

Quality selection applies to streaming sources. A progressive MP4 plays at one fixed quality and exposes no renditions; HLS and DASH streams offer several. See [Media sources](https://videojs.org/docs/framework/react/guides/media-sources).

> **Note**
>
> Using a pre-built [skin](https://videojs.org/docs/framework/react/guides/skins)? It already includes the controls shown here. You may still need the media or player setup in this guide. The component examples are for building your own player UI from individual [components](https://videojs.org/docs/framework/react/guides/ui-components).

## Installation

This guide uses the hls.js media component, so install its adapter with the framework façade:

```bash
pnpm add @videojs/react @videojs/hlsjs-video
```

## Recommended approach

Let automatic quality selection do its job by default, and offer a quality menu for users who want to pin a rendition. Render the menu only when quality selection is available.

**App.tsx**

```tsx
import { Container, createPlayer, Menu } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { QualityRadioGroup } from '@videojs/react/ui/quality-radio-group';
import { videoFeatures } from '@videojs/react/video';
import type { ReactNode } from 'react';

const { Player } = createPlayer({ features: videoFeatures });
const src = 'https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8';

function QualityMenu(): ReactNode {
  return (
    <Menu.Root side="top" align="end">
      <QualityRadioGroup.Root>
        <Menu.Trigger className="settings-trigger" render={<button type="button" />}>
          Quality
          <QualityRadioGroup.Value className="menu-hint" />
        </Menu.Trigger>
        <Menu.Popup className="menu">
          <Menu.Content>
            <QualityRadioGroup.Options
              className="menu-group"
              renderItem={(props, item) => (
                <Menu.RadioItem {...props} className="menu-item">
                  <span>
                    {item.label}
                    {item.tier ? <sup className="menu-tier">{item.tier}</sup> : null}
                  </span>
                  {item.badge ? <span className="menu-badge">{item.badge}</span> : null}
                  <Menu.ItemIndicator checked={item.checked} forceMount className="menu-indicator">
                    ✓
                  </Menu.ItemIndicator>
                </Menu.RadioItem>
              )}
            />
          </Menu.Content>
        </Menu.Popup>
      </QualityRadioGroup.Root>
    </Menu.Root>
  );
}

export default function BasicUsage() {
  return (
    <Player>
      <Container className="media-container">
        <HlsJsVideo src={src} autoPlay crossOrigin="anonymous" muted playsInline loop />
        <div className="menu-bar">
          <QualityMenu />
        </div>
      </Container>
    </Player>
  );
}
```

**App.css**

```css
.media-container {
  position: relative;
}

.media-container video {
  width: 100%;
}

.menu-bar {
  position: absolute;
  right: 10px;
  bottom: 10px;
}

.settings-trigger {
  padding: 6px 16px;
  color: black;
  cursor: pointer;
  background: rgba(255, 255, 255, 0.75);
  border: 1px solid rgba(255, 255, 255, 0.35);
  border-radius: 9999px;
  backdrop-filter: blur(10px);
}

.menu-hint {
  margin-left: 8px;
  color: rgba(0, 0, 0, 0.6);
}

.menu-hint:empty {
  display: none;
}

.menu {
  --media-menu-side-offset: 8px;
  box-sizing: border-box;
  display: grid;
  gap: 2px;
  min-width: 180px;
  max-width: var(--media-menu-available-width, var(--media-popover-available-width, none));
  max-height: var(--media-menu-available-height, var(--media-popover-available-height, none));
  padding: 6px;
  margin: 0;
  overflow: auto;
  overscroll-behavior: none;
  font-size: 14px;
  color: white;
  background: rgba(0, 0, 0, 0.88);
  border: 0;
  border-radius: 8px;
  backdrop-filter: blur(10px);
}

.menu-group {
  display: grid;
  gap: 2px;
}

.menu-item {
  display: flex;
  gap: 8px;
  align-items: center;
  justify-content: space-between;
  min-height: 32px;
  padding: 0 10px;
  font: inherit;
  color: inherit;
  cursor: pointer;
  background: none;
  border: 0;
  border-radius: 6px;
}

.menu-item[data-highlighted] {
  background: rgba(255, 255, 255, 0.16);
}

.menu-tier {
  margin-left: 2px;
  font-size: 10px;
}

.menu-badge {
  margin-left: auto;
  color: rgba(255, 255, 255, 0.72);
}

.menu-indicator {
  opacity: 0;
}

[role="menuitemradio"][aria-checked="true"] .menu-indicator {
  opacity: 1;
}
```

## How it works

The [quality feature](https://videojs.org/docs/framework/react/reference/api/feature-quality) mirrors the media element’s video renditions into player state:

- `videoRenditionList` holds each rendition’s `id`, `width`, `height`, `bitrate`, `frameRate`, `codec`, and whether it’s `selected`.
- `activeVideoRendition` is the rendition currently playing. When the engine doesn’t report one directly, the player matches it from the video’s current dimensions.
- `selectVideoRendition(value)` pins one rendition; pass `'auto'` to return control to the engine’s adaptive selection.

[`useQualityOptions`](https://videojs.org/docs/framework/react/reference/api/use-quality-options) turns this state into ready-made menu options — labels like “1080p”, an “Auto” entry, and `availability` — as the example above shows.

Selecting “Auto” keeps adaptive bitrate switching active: the engine picks the best rendition for current bandwidth and viewport. Pinning a rendition disables adaptation until the user selects “Auto” again.

## Availability and constraints

- Quality availability is `'unavailable'` for media that doesn’t offer a choice — progressive files, streams before the manifest loads, and single-rendition streams. Render quality UI conditionally on that value.
- Renditions come from the streaming engine, so the list depends on what the manifest declares. A single-rendition stream offers no meaningful selection, so availability stays `'unavailable'`.
- Pinning a high rendition on a slow connection causes buffering: the engine can no longer step down. Keep “Auto” the default.
- Rendition lists change on source change; selection resets with them.

## Common variations

### Auto quality only

If you don’t want to expose manual selection, do nothing: adaptive selection is on by default and needs no UI.

### Cap or pin quality programmatically

```tsx
import { Container } from '@videojs/react';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { usePlayer, VideoPlayer } from '@videojs/react/video';

function PinLowestRendition() {
  const store = usePlayer();
  const renditions = usePlayer((s) => s.videoRenditionList);

  const lowest = [...renditions].sort((a, b) => (a.height ?? 0) - (b.height ?? 0))[0];

  return (
    <button type="button" onClick={() => lowest?.id && store.selectVideoRendition(lowest.id)}>
      Data saver
    </button>
  );
}

export default function App() {
  return (
    <VideoPlayer>
      <Container>
        <HlsJsVideo src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM.m3u8" muted playsInline />
        <PinLowestRendition />
      </Container>
    </VideoPlayer>
  );
}
```

## Troubleshooting

### The quality menu doesn’t render

Quality availability is `'unavailable'`. The source is progressive (no renditions), the manifest hasn’t loaded yet, or the media element doesn’t support renditions. Use a streaming media element such as [HlsVideo](https://videojs.org/docs/framework/react/reference/components/hls-video) or [HlsJsVideo](https://videojs.org/docs/framework/react/reference/components/hlsjs-video).

### The menu doesn’t render for a single-rendition stream

The manifest declares a single rendition, so quality availability stays `'unavailable'` — one entry offers no choice. Encode the stream as a multi-rendition ladder to give the engine and users something to choose between.

### Playback buffers after selecting a quality

The pinned rendition exceeds available bandwidth. Selecting “Auto” lets the engine step down again.

## Related pages

### Components

- [QualityRadioGroup](https://videojs.org/docs/framework/react/reference/components/quality-radio-group): A menu radio group for selecting video quality
- [Menu](https://videojs.org/docs/framework/react/reference/components/menu): A composable menu component for settings, option selection, and actions

### API

- [Quality](https://videojs.org/docs/framework/react/reference/api/feature-quality): Video rendition state and actions for the player store
- [useQualityOptions](https://videojs.org/docs/framework/react/reference/api/use-quality-options): Hook to build video quality menu options from the player rendition state
- [HlsVideo](https://videojs.org/docs/framework/react/reference/components/hls-video): Lightweight HLS video element with minimal bundle size
- [HlsJsVideo](https://videojs.org/docs/framework/react/reference/components/hlsjs-video): HLS video element powered by hls.js for adaptive bitrate streaming
- [DashVideo](https://videojs.org/docs/framework/react/reference/components/dash-video): DASH video element powered by dash.js for adaptive bitrate streaming

### Guides

- [Media sources](https://videojs.org/docs/framework/react/guides/media-sources): Set what a media element plays and how its engine plays it with the structured source property

---

React documentation: https://videojs.org/docs/framework/react/llms.txt
All documentation: https://videojs.org/llms.txt
