Component
Vue Video Feed Component
<DomVideoFeed>A vertical video feed with touch scrolling, snap navigation, and one active player. Built for desktop and mobile.
Live example
One video. Then the next.
Swipe or scroll vertically for another video. When focused, use up and down arrows to navigate, space to play or pause, and M for sound.
Video 1 of 3: A slower kind of morning
Swipe or scroll to explore. Sound starts off.
1 of 3
npm install @getdom/studio<script setup>
import '@getdom/studio/style.css';
import { ref } from 'vue';
import { DomIconButton, DomVideoFeed } from '@getdom/studio';
const index = ref(0);
const saved = ref([]);
const videos = [
{
id: 'bunny-meadow',
src: '/media/video-feed/bunny-meadow.mp4',
poster: '/media/video-feed/bunny-meadow.jpg',
title: 'A slower kind of morning',
film: 'Big Buck Bunny',
creator: 'Blender Foundation',
description: 'A little sunshine. A very big rabbit.',
credit: '© 2008 Blender Foundation',
source: 'https://peach.blender.org/about/',
},
{
id: 'sintel-journey',
src: '/media/video-feed/sintel-journey.mp4',
poster: '/media/video-feed/sintel-journey.jpg',
title: 'A world worth getting lost in',
film: 'Sintel',
creator: 'Blender Foundation',
description: 'A glimpse into the open movie’s trailer.',
credit: '© 2010 Blender Foundation',
source: 'https://durian.blender.org/sharing/',
},
{
id: 'bunny-forest',
src: '/media/video-feed/bunny-forest.mp4',
poster: '/media/video-feed/bunny-forest.jpg',
title: 'There’s a story in every frame',
film: 'Big Buck Bunny',
creator: 'Blender Foundation',
description: 'One more moment from the forest.',
credit: '© 2008 Blender Foundation',
source: 'https://peach.blender.org/about/',
},
];
/** Toggles a local saved state; real applications can persist this action in their own store. */
function toggleSave(id) {
const position = saved.value.indexOf(id);
if (position === -1) saved.value.push(id);
else saved.value.splice(position, 1);
}
</script>
<template>
<div class="w-full space-y-4">
<DomVideoFeed v-model:index="index" :items="videos" height="min(42rem, 78svh)" label="Open movie shorts">
<template #caption="{ item }">
<p class="text-[11px] font-semibold uppercase tracking-[0.16em] opacity-80">Blender open movies</p>
<h3 class="text-2xl font-semibold leading-tight tracking-tight">{{ item.title }}</h3>
<p class="text-sm leading-relaxed opacity-90">{{ item.description }}</p>
<p class="pt-1 text-[10px] leading-relaxed opacity-80">
<a :href="item.source" target="_blank" rel="noopener noreferrer" class="underline underline-offset-2">{{ item.film }} · {{ item.credit }}</a>
· <a href="https://creativecommons.org/licenses/by/3.0/" target="_blank" rel="noopener noreferrer" class="underline underline-offset-2">CC BY 3.0</a> · Cropped excerpt
</p>
</template>
<template #actions="{ item }">
<div class="flex flex-col items-center gap-1">
<DomIconButton
:label="saved.includes(item.id) ? 'Remove saved video' : 'Save video'"
:aria-pressed="saved.includes(item.id)"
icon="M6 3h12v18l-6-4-6 4V3Z"
size="lg"
class="!bg-(--video-canvas)/50 !text-(--video-fg) backdrop-blur"
:class="saved.includes(item.id) ? '[&_svg]:fill-current' : ''"
@click="toggleSave(item.id)"
/>
<span class="text-[10px] font-medium">{{ saved.includes(item.id) ? 'Saved' : 'Save' }}</span>
</div>
</template>
</DomVideoFeed>
<div class="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-fg">
<p>Swipe or scroll to explore. Sound starts off.</p>
<p class="tabular-nums">{{ index + 1 }} of {{ videos.length }}</p>
</div>
</div>
</template>
Behaviour
Native scrolling, considered playback
Touch, mouse, and keyboard
Swipe on a phone, scroll with a wheel or trackpad, or use the desktop arrow buttons. Each item snaps into place. Tab into the feed to use its keyboard shortcuts; scrolling beyond either end returns to the surrounding page.
The translucent progress bar uses DomRangeInput. Its handle appears on hover, keyboard focus, or touch dragging. Playback progress follows the media clock between browser media events.
Only the visible video plays
Leaving a video pauses it immediately. Visited videos stay on their last frame and resume from that position when you return. New players load as you approach them. Playback also pauses when less than half the feed is visible or the tab is hidden. Autoplay starts muted and respects reduced motion and data-saving preferences.
Fits a page or an app
The default height is 100dvh. Set height for an embedded feed, or use 100% inside a sized parent. Use fit="contain" to show uncropped landscape sources. Caption and action slots keep your product’s content and behaviour in your application.
Bring your own media
Supply stable IDs and browser-playable video URLs, optional posters, and WebVTT caption tracks. Cross-origin caption sources need CORS headers. Use v-model:index and the change event for navigation and pagination. Exposed methods: goTo(index), next(), and previous().
Open content
Demo films and credits
These demos use Big Buck Bunny, © 2008 Blender Foundation and the Sintel trailer, © 2010 Blender Foundation, licensed under Creative Commons Attribution 3.0.
The local demo assets are short, portrait-cropped, re-encoded excerpts with their original sound. Posters are frames from the same excerpts. Copy the files in public/media/video-feed with this example, preserve attribution, or replace the URLs with your own videos. The media licence is separate from the DOM Studio code licence.
Reference
Props
Control props
| Name | Type | TS | Default | Description |
|---|---|---|---|---|
items | array | Array<unknown> | [] | Video records with a unique id, src, and title. Optional poster, creator, description, objectPosition, and WebVTT tracks. |
index | number | number | 0 | Active item index; supports v-model:index. |
height | string | string | '100dvh' | Feed height. Use a CSS length, or 100% inside an explicitly sized parent. |
muted | boolean | boolean | true | Initial sound preference; supports v-model:muted. Muted playback is most compatible with autoplay. |
autoplay | boolean | boolean | true | Play the visible item automatically, except when reduced motion or data saving is requested. |
loop | boolean | boolean | true | Loop the active video. Does not wrap feed navigation. |
fit | 'cover' | 'contain' | string | 'cover' | Fill the portrait frame or preserve the complete source image. |
label | string | string | 'Video feed' | Accessible name of the feed. |
Auto-generated from Video feed.props and inline _edit hints.
Events
| Name | Payload | Description |
|---|---|---|
| @update:index | ( | The active item changed through scrolling, keyboard, or navigation. |
| @update:muted | ( | The viewer changed the sound preference. |
| @change | ({ item, index }) | Active item changed; use this to request another page near the end. |
| @error | ({ item, index, error }) | A video failed to load or play. Autoplay policy blocks show a play button instead. |
Names auto-detected from defineEmits and source emit() calls; payload and description from __doc.events when present.
Slots
| Name | Scope | Description |
|---|---|---|
| #caption | { item, index } | Replaces the title, creator, and description overlay. Include media attribution here when required. |
| #actions | { item, index } | Application-owned actions beside the caption, such as save or like. |
| #empty | — | Content shown when the feed has no items. |
Keyboard
- ↑ / ↓Previous or next video when the feed is focused.
- Home / EndFirst or last video.
- Space / KPlay or pause.
- MToggle sound.
- TabMove through playback, seek, caption, and application controls.


