Browse docs
Browse docs
A thin, declarative wrapper over the native <video> — the moving-image twin of Image. It renders a URL you give it — no data fetching, no eval — and adds what a bare <video> lacks: it reserves the box before load (no layout shift), shows a poster / skeleton while loading, and degrades to a graceful fallback on error.
import { Video } from '@dashforge/tw';
<Video src="/demo.mp4" poster="/demo.jpg" aspectRatio={16 / 9} rounded="lg" />Give Video a src and a shape — aspectRatio (or a fixed width + height) — so the layout never jumps. A poster shows before playback and doubles as the loading visual. Controls are on by default.
import { Video } from '@dashforge/tw';
// Player with a poster
<Video src={clip} poster={thumb} aspectRatio={16 / 9} rounded="lg" />
// Background-style loop (autoplay needs muted)
<Video src={loop} aspectRatio={1} autoPlay loop muted playsInline controls={false} />
// Multi-format delivery via <source> children
<Video aspectRatio={16 / 9}>
<source src="/clip.webm" type="video/webm" />
<source src="/clip.mp4" type="video/mp4" />
</Video>import { Video } from '@dashforge/tw';
const SRC = 'https://mdn.github.io/shared-assets/videos/flower.mp4';
<Video
src={SRC}
poster="https://picsum.photos/seed/dashforge-video/800/450"
aspectRatio={16 / 9}
rounded="lg"
sx="w-full max-w-md"
/>An aspect-locked player with a poster. Because aspectRatio reserves the box, the layout doesn't shift while the video loads — and the poster fills the space until playback starts.
import { Video } from '@dashforge/tw';
// Background-style loop — autoplay requires muted in most browsers.
<Video
src={SRC}
aspectRatio={16 / 9}
rounded="lg"
autoPlay
loop
muted
playsInline
controls={false}
sx="w-full max-w-md"
/>A muted, looping, controls-free clip — the background-video pattern. Browsers only allow autoPlay when the video is muted; playsInline keeps it inline on iOS instead of going fullscreen.
import { Video, Stack, Typography } from '@dashforge/tw';
<Stack direction="row" gap={3} align="start" sx="w-full">
<Stack gap={1} sx="flex-1">
<Video src="/this-video-does-not-exist.mp4" aspectRatio={16 / 9} rounded="md" />
<Typography variant="caption" color="muted" align="center">default fallback</Typography>
</Stack>
<Stack gap={1} sx="flex-1">
<Video
src="/also-missing.mp4"
aspectRatio={16 / 9}
rounded="md"
fallback={<Typography variant="caption" color="muted">Video unavailable</Typography>}
/>
<Typography variant="caption" color="muted" align="center">custom fallback</Typography>
</Stack>
</Stack>When the source fails to load, Video replaces the player with a muted placeholder. Pass your own via fallback.
Video is Option-C themable — set global defaults for fit and rounded (and slot overrides) via theme.components.Video:
patchTheme({
components: {
Video: { defaults: { rounded: 'md', fit: 'cover' } },
},
});Precedence, lowest to highest: the recipe's defaultVariants → theme.components.Video.defaults → the instance prop → sx / slotProps.
| Prop | Type | Default | Description |
|---|---|---|---|
access | AccessRequirement | — | RBAC access requirement. When the current user is unauthorized: onUnauthorized: 'hide' → the video does not render; 'disable' / 'readonly' → it renders dimmed and non-interactive (a video has no true disabled state). Resolved against the nearest RbacProvider. |
aspectRatio | number | string | — | Locks the box shape before load to prevent layout shift — a number (16 / 9, 1) or a CSS aspect-ratio string ('16 / 9'). When set, the video fills the box via object-fit. |
controls | boolean | true | Show the native player controls. |
fallback | ReactNode | — | Content shown when the video fails to load. Defaults to a muted surface with a broken-media glyph. |
fit | 'none' | 'cover' | 'contain' | 'fill' | 'scale-down' | 'cover' | How the video fills its box — CSS object-fit. |
poster | string | — | Poster image URL shown before playback / while the first frame loads. Doubles as the anti-layout-shift and loading visual. |
preload | 'none' | 'metadata' | 'auto' | 'metadata' | How much to preload. 'metadata' fetches dimensions/duration only. |
rounded | 'md' | 'sm' | 'lg' | 'none' | 'full' | 'none' | Corner radius token, clipped via overflow-hidden. |
showSkeleton | boolean | true | Show a <Skeleton> while the first frame loads. Requires a reserved box (aspectRatio, or both width and height) and no poster (the poster is its own loading visual), else it is skipped. |
slotProps | VideoSlotProps | — | Per-slot overrides. |
src | string | — | Video source URL. Same-origin / data: keeps it inside your boundary; an external host is egress. Omit and pass <source> children for multi-format delivery. |
sx | string | — | Root-level Tailwind override (wins via tailwind-merge). |
visibleWhen | (engine: Engine) => boolean | — | Reactive visibility predicate evaluated against the form engine — the video renders only when it returns true. No-op outside a <DashForm>. |
...rest | Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'className' | 'poster'> | — | Additional native attributes forwarded via Omit<VideoHTMLAttributes<HTMLVideoElement>, 'src' | 'className' | 'poster'>. |
aspectRatio, or both width and height. Without one the video still renders, but the layout shifts as it loads.poster is set it is the loading visual, so the skeleton is skipped. Without a poster, a <Skeleton> fills the reserved box until the first frame is ready (cache-aware — no flash for buffered video).autoPlay with muted (and usually loop + playsInline) for the background-video pattern.<source> children instead of src to let the browser pick the best-supported format.Video is declarative: it points the browser at whatever src you give it. An external host means the browser fetches from that host (egress) — self-host the asset to keep it inside your boundary.access and visibleWhen work exactly as on the form components: hide a clip a user can't see, or reveal it reactively from form state.