Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/calm-videos-play.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@primer/react-brand': minor
---

Added the new `MinimalVideoPlayer` component for presenting muted, looping videos with a minimal play and pause control.

```tsx
<MinimalVideoPlayer poster="/video-poster.jpg" src="/video.webm" title="Product interface demonstration" />
```

🔗 [See `MinimalVideoPlayer` documentation for more usage examples](https://primer.style/brand/components/MinimalVideoPlayer)
120 changes: 120 additions & 0 deletions apps/next-docs/content/components/MinimalVideoPlayer/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
---
title: Minimal video player
description: Use MinimalVideoPlayer for muted, looping product demonstrations with a single play and pause control
keywords: ['playback', 'media', 'video', 'motion']
show-tabs: false
ready: true
source: https://github.com/primer/brand/blob/main/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.tsx
storybook: '/brand/storybook/?path=/story/components-minimalvideoplayer--playground'
thumbnail: '/images/thumbnails/minimal-video-player-thumbnail.png'
thumbnail_darkMode: '/images/thumbnails/minimal-video-player-thumbnail-dark.png'
---

```js
import {MinimalVideoPlayer} from '@primer/react-brand'
```

Use `MinimalVideoPlayer` for short, silent product demonstrations that need only a play and pause control.

Use [VideoPlayer](/components/VideoPlayer) when viewers need audio, captions, seeking, volume, fullscreen controls,
branding, or a dedicated programmatic playback API.

## Examples

### Default

Pass a native video URL with `src`. The required `title` provides a concise accessible name for the video.

```jsx live
<MinimalVideoPlayer
aria-describedby="product-demo-description"
poster="/images/example-poster.png"
src="/example.mp4"
title="Product interface demonstration"
/>
```

### Native source element

The component also accepts native `<source>` children.

```jsx live
<MinimalVideoPlayer poster="/images/example-poster.png" title="Product interface demonstration">
<source src="/example.mp4" type="video/mp4" />
</MinimalVideoPlayer>
```

### Disable automatic playback

`autoPlay` defaults to `true`, making the video eligible to play when it enters the viewport. Set it to `false` when playback should always begin with an explicit user action.

```jsx live
<MinimalVideoPlayer
autoPlay={false}
poster="/images/example-poster.png"
src="/example.mp4"
title="Product interface demonstration"
/>
```

### Disable looping

Videos loop by default. Set `loop` to `false` when the demonstration should stop after one playback.

```jsx live
<MinimalVideoPlayer
loop={false}
poster="/images/example-poster.png"
src="/example.mp4"
title="Product interface demonstration"
/>
```

### Customize control labels

Provide localized play and pause labels with `internalAccessibleLabels`. Keep `title` focused on naming the video rather than the control.

```jsx live
<MinimalVideoPlayer
autoPlay={false}
internalAccessibleLabels={{
play: 'Play product demonstration',
pause: 'Pause product demonstration',
}}
poster="/images/example-poster.png"
src="/example.mp4"
title="Product interface demonstration"
/>
```

## Playback behavior

`MinimalVideoPlayer` is always muted, plays inline, and hides native controls. When `autoPlay` is enabled, it starts only while visible, pauses after leaving the viewport, and resumes after re-entering the viewport unless the viewer paused it manually.

The forwarded ref provides access to the underlying `<video>` element, but the component does not provide a higher-level programmatic playback API.

The component respects the viewer's reduced-motion preference. It suppresses initial automatic playback when reduced motion is requested and pauses playback if that preference becomes active. A viewer can still start the video with the visible play control.

## Accessibility

The `title` prop provides an accessible name for the video. It does not replace a text alternative for meaningful visual content.

For decorative motion or a demonstration that repeats information already communicated by the surrounding content, use a concise `title`. A separate description is not required because the same information is already available on the page.

For a meaningful silent demonstration, provide adjacent text that communicates the important actions and outcomes shown in the video. Associate that text with the video using the native `aria-describedby` attribute, as shown in the [default example](#default).

Use [VideoPlayer](/components/VideoPlayer) instead when the media needs audio, captions, seeking, volume, fullscreen controls, or a standalone viewing experience.

## Component props

`MinimalVideoPlayer` supports standard native `<video>` attributes except the playback attributes controlled by the component.

| Name | Type | Default | Required | Description |
| :------------------------- | :------------------------------ | :------------------------------------------: | :------: | :--------------------------------------------------------------------- |
| `autoPlay` | `boolean` | `true` | `false` | Plays automatically while visible when reduced motion is not requested |
| `children` | `React.ReactNode` | | `false` | Accepts native `<source>` elements |
| `internalAccessibleLabels` | `{play: string, pause: string}` | `{play: 'Play video', pause: 'Pause video'}` | `false` | Sets accessible labels for the play and pause control |
| `loop` | `boolean` | `true` | `false` | Repeats the video after playback ends |
| `poster` | `string` | | `false` | Sets the image shown before playback begins |
| `src` | `string` | | `false` | Sets the native video source URL |
| `title` | `string` | | `true` | Provides an accessible name for the video |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion apps/storybook/static/locales/de/FlexSuiteOverview.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "Hast du Visual Studio Code bereits?",
"trailingLinkText": "Jetzt öffnen",
"peekAlt": "GitHub-Copilot-Maskottchen schaut unten aus dem Rasterbereich hervor",
"imageAlt": "GitHub Copilot im Agent-Modus in VS Code"
"imageAlt": "GitHub Copilot im Agent-Modus in VS Code",
"videoControls": {
"play": "Video abspielen",
"pause": "Video pausieren"
}
},
"logoSuiteHeading": "Partnerlogos von GitHub Copilot",
"resourceCards": {
Expand Down
6 changes: 5 additions & 1 deletion apps/storybook/static/locales/en/FlexSuiteOverview.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "Already have Visual Studio Code?",
"trailingLinkText": "Open now",
"peekAlt": "GitHub Copilot mascot peeking from the bottom of the grid area",
"imageAlt": "GitHub Copilot agent mode in VS Code"
"imageAlt": "GitHub Copilot agent mode in VS Code",
"videoControls": {
"play": "Play video",
"pause": "Pause video"
}
},
"logoSuiteHeading": "GitHub Copilot partner logos",
"resourceCards": {
Expand Down
6 changes: 5 additions & 1 deletion apps/storybook/static/locales/es/FlexSuiteOverview.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "¿Ya tienes Visual Studio Code?",
"trailingLinkText": "Abrir ahora",
"peekAlt": "La mascota de GitHub Copilot asomándose desde la parte inferior del área de cuadrícula",
"imageAlt": "GitHub Copilot en modo agente en VS Code"
"imageAlt": "GitHub Copilot en modo agente en VS Code",
"videoControls": {
"play": "Reproducir video",
"pause": "Pausar video"
}
},
"logoSuiteHeading": "Logotipos de socios de GitHub Copilot",
"resourceCards": {
Expand Down
6 changes: 5 additions & 1 deletion apps/storybook/static/locales/fr/FlexSuiteOverview.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "Vous avez déjà Visual Studio Code ?",
"trailingLinkText": "Ouvrir maintenant",
"peekAlt": "La mascotte de GitHub Copilot apparaît en bas de la zone quadrillée",
"imageAlt": "GitHub Copilot en mode agent dans VS Code"
"imageAlt": "GitHub Copilot en mode agent dans VS Code",
"videoControls": {
"play": "Lire la vidéo",
"pause": "Mettre la vidéo en pause"
}
},
"logoSuiteHeading": "Logos partenaires de GitHub Copilot",
"resourceCards": {
Expand Down
6 changes: 5 additions & 1 deletion apps/storybook/static/locales/ja/FlexSuiteOverview.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "Visual Studio Code はすでにお持ちですか?",
"trailingLinkText": "今すぐ開く",
"peekAlt": "グリッド領域の下からのぞく GitHub Copilot のマスコット",
"imageAlt": "VS Code でエージェントモードを使う GitHub Copilot"
"imageAlt": "VS Code でエージェントモードを使う GitHub Copilot",
"videoControls": {
"play": "動画を再生",
"pause": "動画を一時停止"
}
},
"logoSuiteHeading": "GitHub Copilot のパートナーロゴ",
"resourceCards": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@
"trailingText": "Já tem o Visual Studio Code?",
"trailingLinkText": "Abrir agora",
"peekAlt": "Mascote do GitHub Copilot espiando a partir da parte inferior da área em grade",
"imageAlt": "GitHub Copilot no modo agente no VS Code"
"imageAlt": "GitHub Copilot no modo agente no VS Code",
"videoControls": {
"play": "Reproduzir vídeo",
"pause": "Pausar vídeo"
}
},
"logoSuiteHeading": "Logotipos de parceiros do GitHub Copilot",
"resourceCards": {
Expand Down
22 changes: 11 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/e2e/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
"test:axe": "playwright test --config=scripts/playwright",
"test:visual": "NODE_ENV=test scripts/playwright/run-visual-tests",
"test:visual:update": "NODE_ENV=test scripts/playwright/update-visual-snapshots",
"test:visual:generate": "NODE_ENV=test cd ../../apps/storybook && npx storybook build && cd ../../packages/e2e && npx tsx scripts/playwright/playwright.generate-tests.ts"
"test:visual:generate": "cd ../../apps/storybook && NODE_ENV=test npx storybook build && cd ../../packages/e2e && NODE_ENV=test npx tsx scripts/playwright/playwright.generate-tests.ts"
},
"devDependencies": {
"@github/axe-github": "^0.8.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ const skipTestLookup = [
'components-statistic-features--animations', // animation only
'components-riverstoryscroll-features--video-narrow', // video makes this too flakey
'components-riverstoryscroll-features--video', // video makes this too flakey
'components-minimalvideoplayer--default', // autoplaying video prevents networkidle from settling
'components-minimalvideoplayer--playground', // autoplaying video prevents networkidle from settling
'components-minimalvideoplayer-features--native-source-element', // autoplaying video prevents networkidle from settling
'components-minimalvideoplayer-features--playing', // actively playing video produces nondeterministic frames
'components-minimalvideoplayer-features--custom-accessible-labels', // visually duplicates the paused story
'components-hero-features-images-and-videos--with-native-block-end-default', // for being non-deterministic due to video buffering
'components-hero-features-images-and-videos--with-youtube-video-block-end-default', // for loading a remote video
'components-hero-features-images-and-videos--with-youtube-video-inline-end', // for loading a remote video
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type {Meta, StoryObj} from '@storybook/react'
import React from 'react'
import {expect, userEvent, waitFor, within} from 'storybook/test'

import posterImage from '../fixtures/images/example-poster.png'
import {MinimalVideoPlayer} from '.'

const meta = {
title: 'Components/MinimalVideoPlayer/Features',
component: MinimalVideoPlayer,
} satisfies Meta<typeof MinimalVideoPlayer>

export default meta
type Story = StoryObj<typeof MinimalVideoPlayer>

export const NativeSourceElement: Story = {
render: () => (
<MinimalVideoPlayer poster={posterImage} title="Product interface demonstration">
<source src="./example.mp4" type="video/mp4" />
</MinimalVideoPlayer>
),
}

export const Paused: Story = {
render: () => (
<MinimalVideoPlayer
autoPlay={false}
poster={posterImage}
src="./example.mp4"
title="Product interface demonstration"
/>
),
}

export const Playing: Story = {
...Paused,
play: async ({canvasElement}) => {
const canvas = within(canvasElement)
const video = canvas.getByTitle('Product interface demonstration') as HTMLVideoElement

await userEvent.click(canvas.getByRole('button', {name: 'Play video'}))

await waitFor(() => {
expect(video.paused).toBe(false)
expect(canvas.getByRole('button', {name: 'Pause video'})).toBeVisible()
})
},
}

export const CustomAccessibleLabels: Story = {
render: () => (
<MinimalVideoPlayer
autoPlay={false}
internalAccessibleLabels={{play: 'Play product demonstration', pause: 'Pause product demonstration'}}
poster={posterImage}
src="./example.mp4"
title="Product interface demonstration"
/>
),
}

export const Narrow: Story = {
...Paused,
globals: {
viewport: {value: 'iphonexr'},
},
}
Loading
Loading