diff --git a/.changeset/calm-videos-play.md b/.changeset/calm-videos-play.md
new file mode 100644
index 000000000..f0bd0a008
--- /dev/null
+++ b/.changeset/calm-videos-play.md
@@ -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
+
+```
+
+🔗 [See `MinimalVideoPlayer` documentation for more usage examples](https://primer.style/brand/components/MinimalVideoPlayer)
diff --git a/apps/next-docs/content/components/MinimalVideoPlayer/index.mdx b/apps/next-docs/content/components/MinimalVideoPlayer/index.mdx
new file mode 100644
index 000000000..2f6e7a9bb
--- /dev/null
+++ b/apps/next-docs/content/components/MinimalVideoPlayer/index.mdx
@@ -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
+
+```
+
+### Native source element
+
+The component also accepts native `` children.
+
+```jsx live
+
+
+
+```
+
+### 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
+
+```
+
+### Disable looping
+
+Videos loop by default. Set `loop` to `false` when the demonstration should stop after one playback.
+
+```jsx live
+
+```
+
+### Customize control labels
+
+Provide localized play and pause labels with `internalAccessibleLabels`. Keep `title` focused on naming the video rather than the control.
+
+```jsx live
+
+```
+
+## 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 `` 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 `` 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 `` 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 |
diff --git a/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail-dark.png b/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail-dark.png
new file mode 100644
index 000000000..36bd45e13
Binary files /dev/null and b/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail-dark.png differ
diff --git a/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail.png b/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail.png
new file mode 100644
index 000000000..4c59e099b
Binary files /dev/null and b/apps/next-docs/public/images/thumbnails/minimal-video-player-thumbnail.png differ
diff --git a/apps/storybook/static/locales/de/FlexSuiteOverview.json b/apps/storybook/static/locales/de/FlexSuiteOverview.json
index bdc8b652b..361b0545d 100644
--- a/apps/storybook/static/locales/de/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/de/FlexSuiteOverview.json
@@ -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": {
diff --git a/apps/storybook/static/locales/en/FlexSuiteOverview.json b/apps/storybook/static/locales/en/FlexSuiteOverview.json
index 5a380a0f4..9ac48aa5b 100644
--- a/apps/storybook/static/locales/en/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/en/FlexSuiteOverview.json
@@ -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": {
diff --git a/apps/storybook/static/locales/es/FlexSuiteOverview.json b/apps/storybook/static/locales/es/FlexSuiteOverview.json
index c2700edf4..260ca55e0 100644
--- a/apps/storybook/static/locales/es/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/es/FlexSuiteOverview.json
@@ -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": {
diff --git a/apps/storybook/static/locales/fr/FlexSuiteOverview.json b/apps/storybook/static/locales/fr/FlexSuiteOverview.json
index d799b8e5e..12462b22f 100644
--- a/apps/storybook/static/locales/fr/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/fr/FlexSuiteOverview.json
@@ -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": {
diff --git a/apps/storybook/static/locales/ja/FlexSuiteOverview.json b/apps/storybook/static/locales/ja/FlexSuiteOverview.json
index f903aeba1..5b8a86f7a 100644
--- a/apps/storybook/static/locales/ja/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/ja/FlexSuiteOverview.json
@@ -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": {
diff --git a/apps/storybook/static/locales/pt-BR/FlexSuiteOverview.json b/apps/storybook/static/locales/pt-BR/FlexSuiteOverview.json
index b4fc8f1bd..316d00e8a 100644
--- a/apps/storybook/static/locales/pt-BR/FlexSuiteOverview.json
+++ b/apps/storybook/static/locales/pt-BR/FlexSuiteOverview.json
@@ -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": {
diff --git a/package-lock.json b/package-lock.json
index da2812c60..22f16c165 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -51,7 +51,7 @@
},
"apps/next-docs": {
"name": "@primer/brand-docs",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@primer/doctocat-nextjs": "0.10.0",
@@ -62,9 +62,9 @@
},
"devDependencies": {
"@github/prettier-config": "^0.0.6",
- "@primer/brand-primitives": "^0.69.0",
+ "@primer/brand-primitives": "^0.71.0",
"@primer/react": "38.18.0",
- "@primer/react-brand": "0.69.0",
+ "@primer/react-brand": "0.71.0",
"@swc/core": "^1.15.21",
"@types/node": "24.12.0",
"@types/react": "^19.2.6",
@@ -155,7 +155,7 @@
},
"apps/storybook": {
"name": "@primer/brand-storybook",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@storybook/addon-links": "10.3.4",
@@ -36848,7 +36848,7 @@
},
"packages/css": {
"name": "@primer/brand-css",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "UNLICENSED",
"devDependencies": {
"@types/node": "24.12.0",
@@ -36864,7 +36864,7 @@
},
"packages/design-tokens": {
"name": "@primer/brand-primitives",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"devDependencies": {
"@primer/primitives": "9.1.1",
@@ -36878,7 +36878,7 @@
},
"packages/e2e": {
"name": "@primer/brand-e2e",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"devDependencies": {
"@github/axe-github": "^0.8.1",
@@ -36894,7 +36894,7 @@
},
"packages/fonts": {
"name": "@primer/brand-fonts",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"engines": {
"node": ">=24.0.0",
@@ -36903,7 +36903,7 @@
},
"packages/mcp": {
"name": "@primer/brand-mcp",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.24.0",
@@ -36922,7 +36922,7 @@
},
"packages/react": {
"name": "@primer/react-brand",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT",
"dependencies": {
"@oddbird/popover-polyfill": "0.5.2",
@@ -37069,7 +37069,7 @@
},
"packages/repo-configs": {
"name": "@primer/brand-config",
- "version": "0.69.0",
+ "version": "0.71.0",
"license": "MIT"
}
}
diff --git a/packages/e2e/package.json b/packages/e2e/package.json
index dd530f458..fcd612637 100644
--- a/packages/e2e/package.json
+++ b/packages/e2e/package.json
@@ -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",
diff --git a/packages/e2e/scripts/playwright/playwright.generate-tests.ts b/packages/e2e/scripts/playwright/playwright.generate-tests.ts
index af9958939..aef822cc9 100644
--- a/packages/e2e/scripts/playwright/playwright.generate-tests.ts
+++ b/packages/e2e/scripts/playwright/playwright.generate-tests.ts
@@ -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
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.features.stories.tsx b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.features.stories.tsx
new file mode 100644
index 000000000..ad29dddfc
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.features.stories.tsx
@@ -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
+
+export default meta
+type Story = StoryObj
+
+export const NativeSourceElement: Story = {
+ render: () => (
+
+
+
+ ),
+}
+
+export const Paused: Story = {
+ render: () => (
+
+ ),
+}
+
+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: () => (
+
+ ),
+}
+
+export const Narrow: Story = {
+ ...Paused,
+ globals: {
+ viewport: {value: 'iphonexr'},
+ },
+}
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css
new file mode 100644
index 000000000..c777aa8d3
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css
@@ -0,0 +1,52 @@
+.MinimalVideoPlayer {
+ position: relative;
+ width: 100%;
+}
+
+.MinimalVideoPlayer__video {
+ display: block;
+ width: 100%;
+}
+
+.MinimalVideoPlayer__control {
+ position: absolute;
+ right: var(--base-size-24);
+ bottom: var(--base-size-24);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: var(--base-size-36);
+ height: var(--base-size-36);
+ padding: 0;
+ color: var(--brand-color-text-muted);
+ cursor: pointer;
+ background-color: var(--brand-color-canvas-muted);
+ border: var(--brand-borderWidth-thin) solid var(--brand-color-border-subtle);
+ border-radius: var(--brand-borderRadius-full);
+}
+
+.MinimalVideoPlayer__control:focus-visible {
+ outline: var(--brand-borderWidth-thick) solid var(--brand-color-focus);
+ outline-offset: var(--base-size-2);
+}
+
+/* Progressive enhancement that hides the custom control when native controls are visible. */
+.MinimalVideoPlayer__video[controls] + .MinimalVideoPlayer__control {
+ display: none;
+}
+
+@media (forced-colors: active) {
+ .MinimalVideoPlayer {
+ border: var(--brand-borderWidth-thin) solid CanvasText;
+ }
+
+ .MinimalVideoPlayer__control {
+ color: ButtonText;
+ background-color: ButtonFace;
+ border-color: ButtonText;
+ }
+
+ .MinimalVideoPlayer__control:focus-visible {
+ outline-color: Highlight;
+ }
+}
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css.d.ts b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css.d.ts
new file mode 100644
index 000000000..062befa6d
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.module.css.d.ts
@@ -0,0 +1,7 @@
+declare const styles: {
+ readonly "MinimalVideoPlayer": string;
+ readonly "MinimalVideoPlayer__control": string;
+ readonly "MinimalVideoPlayer__video": string;
+};
+export = styles;
+
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.stories.tsx b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.stories.tsx
new file mode 100644
index 000000000..cdfb0e0c3
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.stories.tsx
@@ -0,0 +1,45 @@
+import type {Meta, StoryObj} from '@storybook/react'
+import React from 'react'
+
+import posterImage from '../fixtures/images/example-poster.png'
+import {MinimalVideoPlayer} from '.'
+
+const meta = {
+ title: 'Components/MinimalVideoPlayer',
+ component: MinimalVideoPlayer,
+ args: {
+ autoPlay: true,
+ loop: true,
+ poster: posterImage,
+ src: './example.mp4',
+ title: 'Product interface demonstration',
+ },
+ argTypes: {
+ autoPlay: {
+ control: {type: 'boolean'},
+ },
+ loop: {
+ control: {type: 'boolean'},
+ },
+ poster: {
+ control: {type: 'text'},
+ },
+ src: {
+ control: {type: 'text'},
+ },
+ title: {
+ control: {type: 'text'},
+ },
+ },
+} satisfies Meta
+
+export default meta
+type Story = StoryObj
+
+export const Default: Story = {
+ render: () => ,
+}
+
+export const Playground: Story = {
+ render: args => ,
+}
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.test.tsx b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.test.tsx
new file mode 100644
index 000000000..9f43200e0
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.test.tsx
@@ -0,0 +1,425 @@
+import React, {createRef} from 'react'
+import {act, cleanup, fireEvent, render} from '@testing-library/react'
+import userEvent from '@testing-library/user-event'
+import '@testing-library/jest-dom'
+import {axe, toHaveNoViolations} from 'jest-axe'
+
+import {MinimalVideoPlayer} from '.'
+
+expect.extend(toHaveNoViolations)
+
+describe('MinimalVideoPlayer', () => {
+ let playMock: jest.SpyInstance
+ let pauseMock: jest.SpyInstance
+ let intersectionObserverCallback: IntersectionObserverCallback
+ let isMediaPaused: boolean
+ let motionPreferenceChangeListener: (event: MediaQueryListEvent) => void
+ let prefersReducedMotion: boolean
+
+ beforeEach(() => {
+ isMediaPaused = true
+ prefersReducedMotion = false
+ motionPreferenceChangeListener = () => undefined
+
+ Object.defineProperty(window, 'matchMedia', {
+ configurable: true,
+ writable: true,
+ value: jest.fn().mockImplementation(query => ({
+ matches: prefersReducedMotion,
+ media: query,
+ onchange: null,
+ addListener: jest.fn(),
+ removeListener: jest.fn(),
+ addEventListener: jest.fn((_type, listener) => {
+ motionPreferenceChangeListener = listener
+ }),
+ removeEventListener: jest.fn(),
+ dispatchEvent: jest.fn(),
+ })),
+ })
+
+ window.IntersectionObserver = jest.fn(callback => {
+ intersectionObserverCallback = callback
+ return {
+ root: null,
+ rootMargin: '',
+ thresholds: [0],
+ disconnect: jest.fn(),
+ observe: jest.fn(),
+ takeRecords: jest.fn(),
+ unobserve: jest.fn(),
+ }
+ })
+
+ jest.spyOn(HTMLMediaElement.prototype, 'paused', 'get').mockImplementation(() => isMediaPaused)
+ playMock = jest.spyOn(HTMLMediaElement.prototype, 'play').mockImplementation(() => {
+ isMediaPaused = false
+ return Promise.resolve()
+ })
+ pauseMock = jest.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {
+ isMediaPaused = true
+ })
+ })
+
+ afterEach(() => {
+ cleanup()
+ jest.restoreAllMocks()
+ })
+
+ const setIntersection = (video: HTMLElement, intersectionRatio: number) => {
+ const rect = video.getBoundingClientRect()
+
+ act(() => {
+ intersectionObserverCallback(
+ [
+ {
+ boundingClientRect: rect,
+ intersectionRect: intersectionRatio > 0 ? rect : new DOMRectReadOnly(),
+ intersectionRatio,
+ isIntersecting: intersectionRatio > 0,
+ rootBounds: null,
+ target: video,
+ time: 0,
+ },
+ ],
+ {} as IntersectionObserver,
+ )
+ })
+ }
+
+ const setReducedMotion = (matches: boolean) => {
+ prefersReducedMotion = matches
+ act(() => {
+ motionPreferenceChangeListener({matches} as MediaQueryListEvent)
+ })
+ }
+
+ it('renders native video sources and attributes', () => {
+ const {getByTitle} = render(
+
+
+ ,
+ )
+
+ const video = getByTitle('Product interface demonstration')
+
+ expect(video).toHaveAttribute('src', '/example.webm')
+ expect(video).toHaveAttribute('poster', '/example-poster.jpg')
+ expect(video).toHaveAttribute('preload', 'metadata')
+ expect(video).toHaveClass('custom-video')
+ expect(video.querySelector('source')).toHaveAttribute('src', '/example.mp4')
+ expect(video.querySelector('source')).toHaveAttribute('type', 'video/mp4')
+ })
+
+ it('enforces minimal playback attributes and defaults autoplay eligibility and looping on', () => {
+ const {getByTitle} = render( )
+
+ const video = getByTitle('Product interface demonstration') as HTMLVideoElement
+
+ expect(video.autoplay).toBe(false)
+ expect(video.loop).toBe(true)
+ expect(video.muted).toBe(true)
+ expect(video.playsInline).toBe(true)
+ expect(video.controls).toBe(false)
+ expect(video).not.toHaveAttribute('controls')
+ })
+
+ it('allows autoplay eligibility and looping to be disabled', () => {
+ const {getByTitle} = render(
+ ,
+ )
+
+ const video = getByTitle('Product interface demonstration') as HTMLVideoElement
+
+ expect(video.autoplay).toBe(false)
+ expect(video.loop).toBe(false)
+ })
+
+ it('autoplays an eligible video when it is visible', () => {
+ const {getByTitle} = render( )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('autoplays an eligible video when IntersectionObserver is unavailable', () => {
+ Object.defineProperty(window, 'IntersectionObserver', {
+ configurable: true,
+ writable: true,
+ value: undefined,
+ })
+
+ render( )
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('waits for an initially offscreen video to become visible before autoplaying', () => {
+ const {getByTitle} = render( )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 0)
+ expect(playMock).not.toHaveBeenCalled()
+
+ setIntersection(video, 0.01)
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('uses the current autoPlay value when visibility is first observed', () => {
+ const {getByTitle, rerender} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ rerender( )
+ setIntersection(video, 1)
+
+ expect(playMock).not.toHaveBeenCalled()
+
+ rerender( )
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('pauses fully offscreen playback and resumes it on re-entry', () => {
+ const {getByTitle} = render( )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ fireEvent.playing(video)
+ setIntersection(video, 0)
+
+ expect(pauseMock).toHaveBeenCalledTimes(1)
+
+ fireEvent.pause(video)
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(2)
+ })
+
+ it('pauses offscreen playback before the playing event updates component state', () => {
+ const {getByTitle} = render( )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ setIntersection(video, 0)
+
+ expect(pauseMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not resume after a manual pause', async () => {
+ const user = userEvent.setup()
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ fireEvent.playing(video)
+ await user.click(getByRole('button', {name: 'Pause video'}))
+ fireEvent.pause(video)
+ setIntersection(video, 0)
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not resume when it leaves the viewport before a manual pause event fires', async () => {
+ const user = userEvent.setup()
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ fireEvent.playing(video)
+ await user.click(getByRole('button', {name: 'Pause video'}))
+ setIntersection(video, 0)
+ fireEvent.pause(video)
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('does not autoplay or viewport-resume when autoplay is disabled', async () => {
+ const user = userEvent.setup()
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ expect(playMock).not.toHaveBeenCalled()
+
+ await user.click(getByRole('button', {name: 'Play video'}))
+ fireEvent.playing(video)
+ setIntersection(video, 0)
+ fireEvent.pause(video)
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('suppresses initial autoplay when reduced motion is requested', async () => {
+ prefersReducedMotion = true
+ const user = userEvent.setup()
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ setReducedMotion(false)
+
+ expect(playMock).not.toHaveBeenCalled()
+
+ await user.click(getByRole('button', {name: 'Play video'}))
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('pauses on a live reduced-motion preference and does not resume later', () => {
+ const {getByTitle} = render( )
+ const video = getByTitle('Product interface demonstration')
+
+ setIntersection(video, 1)
+ fireEvent.playing(video)
+ setReducedMotion(true)
+
+ expect(pauseMock).toHaveBeenCalledTimes(1)
+
+ fireEvent.pause(video)
+ setReducedMotion(false)
+ setIntersection(video, 0)
+ setIntersection(video, 1)
+
+ expect(playMock).toHaveBeenCalledTimes(1)
+ })
+
+ it('preserves native playback event handlers', () => {
+ const onPlay = jest.fn()
+ const onPlaying = jest.fn()
+ const onPause = jest.fn()
+ const onEnded = jest.fn()
+ const {getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ fireEvent.play(video)
+ fireEvent.playing(video)
+ fireEvent.pause(video)
+ fireEvent.ended(video)
+
+ expect(onPlay).toHaveBeenCalledTimes(1)
+ expect(onPlaying).toHaveBeenCalledTimes(1)
+ expect(onPause).toHaveBeenCalledTimes(1)
+ expect(onEnded).toHaveBeenCalledTimes(1)
+ })
+
+ it('uses a custom root test ID and keeps the video test ID stable', () => {
+ const {getByTestId} = render(
+ ,
+ )
+
+ expect(getByTestId('custom-video')).toContainElement(getByTestId(MinimalVideoPlayer.testIds.video))
+ expect(getByTestId(MinimalVideoPlayer.testIds.video)).toHaveAttribute('title', 'Product interface demonstration')
+ })
+
+ it('forwards its ref to the video element', () => {
+ const ref = createRef()
+
+ render( )
+
+ expect(ref.current).toBeInstanceOf(HTMLVideoElement)
+ expect(ref.current).toHaveAttribute('title', 'Product interface demonstration')
+ })
+
+ it('uses overridable internal accessible labels', () => {
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+
+ expect(getByRole('button', {name: 'Start animation'})).toBeInTheDocument()
+
+ fireEvent.play(getByTitle('Product interface demonstration'))
+
+ expect(getByRole('button', {name: 'Stop animation'})).toBeInTheDocument()
+ })
+
+ it('plays and pauses only through the overlaid control', async () => {
+ const user = userEvent.setup()
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ fireEvent.click(video)
+ expect(playMock).not.toHaveBeenCalled()
+ expect(pauseMock).not.toHaveBeenCalled()
+
+ await user.click(getByRole('button', {name: 'Play video'}))
+ expect(playMock).toHaveBeenCalledTimes(1)
+ expect(getByRole('button', {name: 'Play video'})).toBeInTheDocument()
+
+ fireEvent.playing(video)
+ expect(getByRole('button', {name: 'Pause video'})).toBeInTheDocument()
+
+ await user.click(getByRole('button', {name: 'Pause video'}))
+ expect(pauseMock).toHaveBeenCalledTimes(1)
+ expect(getByRole('button', {name: 'Play video'})).toBeInTheDocument()
+
+ fireEvent.pause(video)
+ expect(getByRole('button', {name: 'Play video'})).toBeInTheDocument()
+ })
+
+ it('returns to the paused control state when playback ends', () => {
+ const {getByRole, getByTitle} = render(
+ ,
+ )
+ const video = getByTitle('Product interface demonstration')
+
+ fireEvent.play(video)
+ expect(getByRole('button', {name: 'Pause video'})).toBeInTheDocument()
+
+ fireEvent.ended(video)
+ expect(getByRole('button', {name: 'Play video'})).toBeInTheDocument()
+ })
+
+ it('keeps the paused control state when playback fails', async () => {
+ playMock.mockRejectedValueOnce(new DOMException('Playback was prevented', 'NotAllowedError'))
+ const user = userEvent.setup()
+ const {getByRole} = render( )
+
+ await user.click(getByRole('button', {name: 'Play video'}))
+
+ expect(getByRole('button', {name: 'Play video'})).toBeInTheDocument()
+ })
+
+ it('has no accessibility violations', async () => {
+ const {container} = render( )
+
+ const results = await axe(container)
+
+ expect(results).toHaveNoViolations()
+ })
+})
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.tsx b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.tsx
new file mode 100644
index 000000000..7e2d4896e
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.tsx
@@ -0,0 +1,128 @@
+import {clsx} from 'clsx'
+import {PauseIcon} from '@primer/octicons-react'
+import React, {forwardRef, type ComponentPropsWithoutRef} from 'react'
+
+import {useReducedMotion} from '../hooks/useReducedMotion'
+import {useMinimalVideoPlayback} from './useMinimalVideoPlayback'
+import styles from './MinimalVideoPlayer.module.css'
+
+export type MinimalVideoPlayerProps = Omit<
+ ComponentPropsWithoutRef<'video'>,
+ 'autoPlay' | 'controls' | 'defaultMuted' | 'muted' | 'onVolumeChange' | 'playsInline' | 'title'
+> & {
+ /**
+ * Plays the video when it is visible.
+ * @default true
+ */
+ autoPlay?: boolean
+ /**
+ * Provides an accessible name for the video.
+ */
+ title: string
+ /**
+ * Customizable labels for the internal play and pause control.
+ */
+ internalAccessibleLabels?: {
+ play: string
+ pause: string
+ }
+ ['data-testid']?: string
+}
+
+const defaultInternalAccessibleLabels = {
+ play: 'Play video',
+ pause: 'Pause video',
+}
+
+const testIds = {
+ root: 'MinimalVideoPlayer',
+ get video() {
+ return `${this.root}-video`
+ },
+ get control() {
+ return `${this.root}-control`
+ },
+}
+
+export const _MinimalVideoPlayer = forwardRef(
+ (
+ {
+ autoPlay = true,
+ children,
+ className,
+ 'data-testid': testId,
+ internalAccessibleLabels = defaultInternalAccessibleLabels,
+ loop = true,
+ onEnded,
+ onPause,
+ onPlay,
+ onPlaying,
+ title,
+ ...rest
+ },
+ forwardedRef,
+ ) => {
+ const prefersReducedMotion = useReducedMotion()
+ const {handlePlaybackEnded, handlePlaybackPaused, handlePlaybackStarted, isPlaying, togglePlayback, videoRef} =
+ useMinimalVideoPlayback({
+ autoPlay,
+ forwardedRef,
+ prefersReducedMotion,
+ })
+
+ return (
+
+
{
+ handlePlaybackEnded()
+ onEnded?.(event)
+ }}
+ onPause={event => {
+ handlePlaybackPaused()
+ onPause?.(event)
+ }}
+ onPlay={event => {
+ handlePlaybackStarted()
+ onPlay?.(event)
+ }}
+ onPlaying={event => {
+ handlePlaybackStarted()
+ onPlaying?.(event)
+ }}
+ playsInline
+ title={title}
+ >
+ {children}
+
+
+ {isPlaying ? (
+
+ ) : (
+
+
+
+ )}
+
+
+ )
+ },
+)
+
+export const MinimalVideoPlayer = Object.assign(_MinimalVideoPlayer, {testIds})
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts
new file mode 100644
index 000000000..c58b98f8a
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts
@@ -0,0 +1,35 @@
+/*
+ * Do not modify this file directly.
+ * This file was generated by: playwright.generate-tests.ts.
+ * Regenerate using: npm run test:visual:generate
+ */
+import {test, expect} from '@playwright/test'
+
+// eslint-disable-next-line i18n-text/no-en
+test.describe('Visual Comparison: MinimalVideoPlayer', () => {
+ test('MinimalVideoPlayer / Paused', async ({page}) => {
+ await page.goto(
+ 'http://localhost:6006/iframe.html?args=&id=components-minimalvideoplayer-features--paused&viewMode=story',
+ {waitUntil: 'networkidle'},
+ )
+ await page.locator('body.sb-show-main').waitFor({state: 'visible'})
+
+ await page.waitForTimeout(500)
+ await expect(page).toHaveScreenshot({fullPage: true})
+ })
+
+ // eslint-disable-next-line i18n-text/no-en
+ test.describe('Mobile viewport test for Narrow', () => {
+ test.use({viewport: {width: 360, height: 800}})
+ test('MinimalVideoPlayer / Narrow', async ({page}) => {
+ await page.goto(
+ 'http://localhost:6006/iframe.html?args=&id=components-minimalvideoplayer-features--narrow&viewMode=story',
+ {waitUntil: 'networkidle'},
+ )
+ await page.locator('body.sb-show-main').waitFor({state: 'visible'})
+
+ await page.waitForTimeout(500)
+ await expect(page).toHaveScreenshot({fullPage: true})
+ })
+ })
+})
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-MinimalVideoPlayer-Paused-1-darwin.png b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-MinimalVideoPlayer-Paused-1-darwin.png
new file mode 100644
index 000000000..8c00c5578
Binary files /dev/null and b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-MinimalVideoPlayer-Paused-1-darwin.png differ
diff --git a/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-Mobile-viewport-test-for-Narrow-MinimalVideoPlayer-Narrow-1-darwin.png b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-Mobile-viewport-test-for-Narrow-MinimalVideoPlayer-Narrow-1-darwin.png
new file mode 100644
index 000000000..68db35b08
Binary files /dev/null and b/packages/react/src/MinimalVideoPlayer/MinimalVideoPlayer.visual.spec.ts-snapshots/Visual-Comparison-MinimalVideoPlayer-Mobile-viewport-test-for-Narrow-MinimalVideoPlayer-Narrow-1-darwin.png differ
diff --git a/packages/react/src/MinimalVideoPlayer/index.ts b/packages/react/src/MinimalVideoPlayer/index.ts
new file mode 100644
index 000000000..0d3af5afb
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/index.ts
@@ -0,0 +1 @@
+export * from './MinimalVideoPlayer'
diff --git a/packages/react/src/MinimalVideoPlayer/useMinimalVideoPlayback.ts b/packages/react/src/MinimalVideoPlayer/useMinimalVideoPlayback.ts
new file mode 100644
index 000000000..69e625554
--- /dev/null
+++ b/packages/react/src/MinimalVideoPlayer/useMinimalVideoPlayback.ts
@@ -0,0 +1,121 @@
+import {useCallback, useEffect, useRef, useState, type Ref} from 'react'
+
+import {useProvidedRefOrCreate} from '../hooks/useRef'
+
+type UseMinimalVideoPlaybackOptions = {
+ autoPlay: boolean
+ forwardedRef: Ref
+ prefersReducedMotion: boolean
+}
+
+export function useMinimalVideoPlayback({
+ autoPlay,
+ forwardedRef,
+ prefersReducedMotion,
+}: UseMinimalVideoPlaybackOptions) {
+ const videoRef = useProvidedRefOrCreate(forwardedRef)
+ const pausedByViewportRef = useRef(false)
+ const initialAutoPlayHandledRef = useRef(false)
+ const [isPlaying, setIsPlaying] = useState(false)
+
+ const requestPlayback = useCallback(async () => {
+ const video = videoRef.current
+
+ if (!video) return
+
+ try {
+ await video.play()
+ } catch {
+ if (video.paused) {
+ setIsPlaying(false)
+ }
+ }
+ }, [videoRef])
+
+ const handlePlaybackStarted = useCallback(() => setIsPlaying(true), [])
+ const handlePlaybackPaused = useCallback(() => setIsPlaying(false), [])
+ const handlePlaybackEnded = useCallback(() => {
+ pausedByViewportRef.current = false
+ setIsPlaying(false)
+ }, [])
+
+ useEffect(() => {
+ const video = videoRef.current
+
+ if (!video) return
+
+ const handleVisibilityChange = (isVisible: boolean) => {
+ if (isVisible) {
+ if (pausedByViewportRef.current) {
+ pausedByViewportRef.current = false
+ if (autoPlay && !prefersReducedMotion) {
+ requestPlayback()
+ }
+ return
+ }
+
+ if (!initialAutoPlayHandledRef.current && prefersReducedMotion) {
+ initialAutoPlayHandledRef.current = true
+ return
+ }
+
+ if (!initialAutoPlayHandledRef.current && autoPlay) {
+ initialAutoPlayHandledRef.current = true
+ requestPlayback()
+ }
+ return
+ }
+
+ if (!video.paused) {
+ pausedByViewportRef.current = autoPlay && !prefersReducedMotion
+ video.pause()
+ }
+ }
+
+ if (typeof IntersectionObserver === 'undefined') {
+ handleVisibilityChange(true)
+ return
+ }
+
+ const observer = new IntersectionObserver(([entry]) => {
+ handleVisibilityChange(entry.intersectionRatio > 0)
+ })
+
+ observer.observe(video)
+ return () => observer.disconnect()
+ }, [autoPlay, prefersReducedMotion, requestPlayback, videoRef])
+
+ useEffect(() => {
+ if (!prefersReducedMotion) return
+
+ initialAutoPlayHandledRef.current = true
+ pausedByViewportRef.current = false
+ videoRef.current?.pause()
+ }, [prefersReducedMotion, videoRef])
+
+ const togglePlayback = useCallback(() => {
+ const video = videoRef.current
+
+ if (!video) return
+
+ initialAutoPlayHandledRef.current = true
+ pausedByViewportRef.current = false
+
+ if (isPlaying) {
+ setIsPlaying(false)
+ video.pause()
+ return
+ }
+
+ requestPlayback()
+ }, [isPlaying, requestPlayback, videoRef])
+
+ return {
+ handlePlaybackEnded,
+ handlePlaybackPaused,
+ handlePlaybackStarted,
+ isPlaying,
+ togglePlayback,
+ videoRef,
+ }
+}
diff --git a/packages/react/src/fixtures/videos/copilot-agent-mode-hero.mp4 b/packages/react/src/fixtures/videos/copilot-agent-mode-hero.mp4
new file mode 100644
index 000000000..20cdd29e1
Binary files /dev/null and b/packages/react/src/fixtures/videos/copilot-agent-mode-hero.mp4 differ
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index 5fec2dbab..b41a418e7 100644
--- a/packages/react/src/index.ts
+++ b/packages/react/src/index.ts
@@ -36,6 +36,7 @@ export * from './animation'
export * from './Prose'
export * from './Box'
export * from './VideoPlayer'
+export * from './MinimalVideoPlayer'
export * from './LogoSuite'
export * from './Timeline'
export * from './Bento'
diff --git a/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.content.ts b/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.content.ts
index ea254f0f7..45b46886a 100644
--- a/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.content.ts
+++ b/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.content.ts
@@ -59,6 +59,10 @@ export type FlexSuiteAIOverviewContent = {
trailingLinkText: string
peekAlt: string
imageAlt: string
+ videoControls: {
+ play: string
+ pause: string
+ }
}
logoSuiteHeading: string
resourceCards: FlexSuiteAIOverviewCardContent[]
@@ -121,6 +125,10 @@ export const defaultFlexSuiteAIOverviewContent: FlexSuiteAIOverviewContent = {
trailingLinkText: 'Open now',
peekAlt: 'GitHub Copilot mascot peeking from the bottom of the grid area',
imageAlt: 'GitHub Copilot agent mode in VS Code',
+ videoControls: {
+ play: 'Play video',
+ pause: 'Pause video',
+ },
},
logoSuiteHeading: 'GitHub Copilot partner logos',
resourceCards: [
@@ -362,6 +370,10 @@ export function getLocalizedFlexSuiteAIOverviewContent(t: Translate): FlexSuiteA
trailingLinkText: t('hero.trailingLinkText'),
peekAlt: t('hero.peekAlt'),
imageAlt: t('hero.imageAlt'),
+ videoControls: {
+ play: t('hero.videoControls.play'),
+ pause: t('hero.videoControls.pause'),
+ },
},
logoSuiteHeading: t('logoSuiteHeading'),
resourceCards: [
diff --git a/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.tsx b/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.tsx
index 2403eeeee..ab8f1b595 100644
--- a/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.tsx
+++ b/packages/react/src/recipes/Flexsuite/Overview/FlexSuiteAIOverview.tsx
@@ -15,6 +15,7 @@ import {
Heading,
InlineLink,
LogoSuite,
+ MinimalVideoPlayer,
Section,
SectionIntro,
SubNav,
@@ -38,6 +39,7 @@ import mercardo from '../../../fixtures/images/bento/mercado.png'
import copilotHeroHead from '../../../fixtures/images/copilot-mascot-head-peeking.png'
import renderUI3 from '../../../fixtures/images/copilot-vscode-agent-mode-3.png'
import renderUI3Dark from '../../../fixtures/images/copilot-vscode-agent-mode-3-dark.png'
+import heroAnimation from '../../../fixtures/videos/copilot-agent-mode-hero.mp4'
import {logos} from '../../../LogoSuite/LogoSuite.fixtures'
import {tileItems} from '../../../Tiles/Tiles.fixtures'
@@ -133,12 +135,23 @@ export function FlexSuiteAIOverviewTemplate({content}: FlexSuiteAIOverviewTempla
{content.hero.secondaryAction}
-
+ {process.env.NODE_ENV === 'test' ? (
+
+ ) : (
+
+
+
+ )}
diff --git a/packages/react/src/recipes/Flexsuite/Overview/Overview.visual.spec.ts-snapshots/Visual-Comparison-Overview-Overview-AI-1-darwin.png b/packages/react/src/recipes/Flexsuite/Overview/Overview.visual.spec.ts-snapshots/Visual-Comparison-Overview-Overview-AI-1-darwin.png
index 93ebf47aa..0762ddc30 100644
Binary files a/packages/react/src/recipes/Flexsuite/Overview/Overview.visual.spec.ts-snapshots/Visual-Comparison-Overview-Overview-AI-1-darwin.png and b/packages/react/src/recipes/Flexsuite/Overview/Overview.visual.spec.ts-snapshots/Visual-Comparison-Overview-Overview-AI-1-darwin.png differ
diff --git a/packages/react/src/river/RiverBreakoutTabs/RiverBreakoutTabs.visual.spec.ts-snapshots/Visual-Comparison-RiverBreakoutTabs-RiverBreakoutTabs-With-images-1-darwin.png b/packages/react/src/river/RiverBreakoutTabs/RiverBreakoutTabs.visual.spec.ts-snapshots/Visual-Comparison-RiverBreakoutTabs-RiverBreakoutTabs-With-images-1-darwin.png
index 8f7bbd43c..f9764d91a 100644
Binary files a/packages/react/src/river/RiverBreakoutTabs/RiverBreakoutTabs.visual.spec.ts-snapshots/Visual-Comparison-RiverBreakoutTabs-RiverBreakoutTabs-With-images-1-darwin.png and b/packages/react/src/river/RiverBreakoutTabs/RiverBreakoutTabs.visual.spec.ts-snapshots/Visual-Comparison-RiverBreakoutTabs-RiverBreakoutTabs-With-images-1-darwin.png differ