# 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/html/guides/media-sources).

> **Note**
>
> Using a pre-built [skin](https://videojs.org/docs/framework/html/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/html/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/html @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.

**index.html**

```html
<video-player class="video-player">
  <media-container>
    <hlsjs-video
      class="video-media"
      src="https://stream.mux.com/lhnU49l1VGi3zrTAZhDm9LUUxSjpaPW9BL4jY25Kwo4.m3u8"
      autoplay
      crossorigin="anonymous"
      muted
      playsinline
      loop
    ></hlsjs-video>
    <div class="menu-bar">
      <button type="button" commandfor="quality-menu" class="settings-trigger">Quality</button>
      <media-menu id="quality-menu" side="top" align="end" class="menu">
        <media-menu-content>
          <media-quality-radio-group class="menu-group" label="Quality">
            <template>
              <media-menu-radio-item class="menu-item">
                <span>
                  <span data-part="label"></span>
                  <sup data-part="tier" class="menu-tier"></sup>
                </span>
                <span data-part="badge" class="menu-badge"></span>
                <media-menu-item-indicator force-mount class="menu-indicator">✓</media-menu-item-indicator>
              </media-menu-radio-item>
            </template>
          </media-quality-radio-group>
        </media-menu-content>
      </media-menu>
    </div>
  </media-container>
</video-player>
```

**index.css**

```css
.video-player media-container {
  position: relative;
  display: block;
}

.video-media {
  width: 100%;
}

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

.menu-bar:has([data-availability="unavailable"]) {
  display: none;
}

.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 {
  --media-menu-side-offset: 8px;
  box-sizing: border-box;
  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;
  overflow: auto;
  overscroll-behavior: none;
  font-size: 14px;
  color: white;
  background: rgba(0, 0, 0, 0.88);
  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;
}

media-menu-radio-item[aria-checked="true"] .menu-indicator {
  opacity: 1;
}
```

**index.ts**

```ts
import '@videojs/html/video/player';
import '@videojs/html/ui/container';
import '@videojs/html/media/hlsjs-video';
import '@videojs/html/ui/menu';
import '@videojs/html/ui/menu-content';
import '@videojs/html/ui/menu-radio-item';
import '@videojs/html/ui/menu-item-indicator';
import '@videojs/html/ui/quality-radio-group';
```

## How it works

The [quality feature](https://videojs.org/docs/framework/html/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.

[`<media-quality-radio-group>`](https://videojs.org/docs/framework/html/reference/components/quality-radio-group) turns this state into a ready-made radio menu with resolution labels and an “Auto” entry, 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

Access the player store through the [player controller](https://videojs.org/docs/framework/html/reference/api/player-controller) and call `selectVideoRendition` with a rendition `id` from `videoRenditionList`, or `'auto'` to restore adaptive selection.

## 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 [`<hls-video>`](https://videojs.org/docs/framework/html/reference/components/hls-video) or [`<hlsjs-video>`](https://videojs.org/docs/framework/html/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

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

### API

- [Quality](https://videojs.org/docs/framework/html/reference/api/feature-quality): Video rendition state and actions for the player store
- [hls-video](https://videojs.org/docs/framework/html/reference/components/hls-video): Lightweight HLS video element with minimal bundle size
- [hlsjs-video](https://videojs.org/docs/framework/html/reference/components/hlsjs-video): HLS video element powered by hls.js for adaptive bitrate streaming
- [dash-video](https://videojs.org/docs/framework/html/reference/components/dash-video): DASH video element powered by dash.js for adaptive bitrate streaming

### Guides

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

---

HTML documentation: https://videojs.org/docs/framework/html/llms.txt
All documentation: https://videojs.org/llms.txt
