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

Install
npm install @getdom/studio
vue
<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.

Source downloads, timestamps, and attribution notes

Reference

Props

Control props

NameTypeTSDefaultDescription
itemsarrayArray<unknown>[]Video records with a unique id, src, and title. Optional poster, creator, description, objectPosition, and WebVTT tracks.
indexnumbernumber0Active item index; supports v-model:index.
heightstringstring'100dvh'Feed height. Use a CSS length, or 100% inside an explicitly sized parent.
mutedbooleanbooleantrueInitial sound preference; supports v-model:muted. Muted playback is most compatible with autoplay.
autoplaybooleanbooleantruePlay the visible item automatically, except when reduced motion or data saving is requested.
loopbooleanbooleantrueLoop the active video. Does not wrap feed navigation.
fit'cover' | 'contain'string'cover'Fill the portrait frame or preserve the complete source image.
labelstringstring'Video feed'Accessible name of the feed.

Auto-generated from Video feed.props and inline _edit hints.

Events

NamePayloadDescription
@update:index(index
index: number;
: number)
The active item changed through scrolling, keyboard, or navigation.
@update:muted(muted
muted: boolean;
: boolean)
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

NameScopeDescription
#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.
#emptyContent 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.