HtVideo
Video that plays in the mode set by playback: native controls, a still first frame, a muted loop, or a preview under the mouse. It requires alt or decorative. A decorative video is hidden from screen readers, shows no controls, and shows its first frame unless playback is set.
Use HtMedia when the URL can be an image or a video.
Example
Usage
playback sets how the video plays. Pick one value, then add the props that value accepts. TypeScript rejects a prop on a value that does not accept it.
click(default): native controls, and the viewer presses play.autoplay: a muted loop that starts on its own.none: the first frame, and it never plays.hover: a muted loop while the mouse is over the video.scrub: the frame under the mouse.
click and autoplay are players. none, hover, and scrub are thumbnails. The duration badge works only on the thumbnail values, and the sections after it apply to every value.
Playback: click
The default. Native controls show, and the viewer starts playback. Use it when the video is the content, such as a product walkthrough in a dialog.
muted starts the video without sound. On click, it defaults to false.
controlsList hides parts of the native controls, on click and on autoplay with controls. It takes the native tokens nodownload, nofullscreen, and noremoteplayback, separated by spaces. Chromium browsers support it, and Firefox ignores it. controlsList on MDN lists current support.
Playback: autoplay
The video plays on its own, muted, on a loop, with no controls. Use it for a short ambient clip, such as an illustration on a splash page. If the viewer prefers reduced motion, the video shows its first frame.
controls shows native controls, so the viewer can pause the loop. Use it when the loop carries content, such as a product demo in a chat message. Under reduced motion, the video shows its first frame with the controls, so the viewer can still play it.
loop restarts the video when it ends, and defaults to true. Set loop={false} for a clip that plays once and stops on its last frame, such as an intro animation.
muted defaults to true on autoplay, because browsers block autoplay with sound. Set muted={false} only when the viewer opened the video with a click, such as a preview that plays when it opens. Without that click, the browser can refuse to play it, and the video stays on its first frame.
autoplay also accepts controlsList.
Playback: none
The first frame, or poster when set, and the video never plays. Use it for a thumbnail inside a clickable card, so the card gets the click instead of the native controls. It accepts the duration badge.
Playback: hover
A muted loop plays while the mouse is over the video, and the first frame returns when the mouse leaves. Use it for a thumbnail in a grid that previews its motion, such as a creative in a library. On touch, and under reduced motion, the video shows its first frame, or poster when set. It accepts the duration badge.
Move the mouse over the video.
Playback: scrub
The frame under the mouse shows. The left edge is the start, the right edge is the end, and a line marks the position. Use it for a thumbnail where the viewer compares frames across a long video, such as an ad in a gallery. The video is muted, and the first frame returns when the mouse leaves. On touch, the video shows its first frame, or poster when set. It accepts the duration badge.
Move the mouse across the video.
showScrubTime shows the time of the frame on screen in the badge while the viewer scrubs, and the length at rest. Use it when the viewer picks a moment in a clip, such as in an asset library. It shows the badge without showDuration.
An element that you place over a hover or scrub video, such as a label, needs pointerEvents="none". Otherwise the mouse leaves the video when it moves over the element.
Duration badge
showDuration shows the length of the video in a dark badge in the bottom corner, once the video loads its metadata. It works with none, hover, and scrub, which have no native timecode. While the viewer scrubs, the badge hides, so it does not cover the frame.
Sources
Pass src for one video file. To give the browser more than one format, pass <source> elements as children instead, and the browser plays the first one it supports. In this demo, browsers that play WebM show its transparent background, and the others fall back to the MP4 on white.
Captions
Pass <track> elements as children for captions. Add a captions track when the video has speech.
Poster
poster shows an image before the video plays or loads. Set it on none, hover, and scrub thumbnails when you have one, because Safari can show an empty first frame for a video that has not played.
Lazy loading
loading="lazy" waits to load the video until it comes within 200px of the viewport. Once loaded, the video stays loaded, so scrolling back does not fetch it again. Use it for a grid of many videos, where loading all of them at once blocks other requests. With <source> children, the children wait too.
Loading
Set showSkeleton when the video loads slowly and the space around it must not jump. A skeleton fills the slot until the video has its first frame, then the video fades in. Set skeletonHeight to match the expected video height. This demo video takes a few seconds to load.
The controls stay hidden until the video loads, so a viewer cannot tab to a player they cannot see. With loading="lazy", the skeleton shows until the video comes near the viewport and loads.
Error
Set showError to replace a video that fails to load with an error message, instead of an empty player. The message takes the video's size, border radius, and margin props. Pass onRetry to add a Refresh button, for example to request a new signed URL. The message shows for a failed src. A failed <source> child does not show it, because the next source can still play.
Styling
HtVideo adds no visual styling of its own, apart from a focus ring, the scrub line, and the duration badge. Size and frame each video with style props, such as width, maxWidth, borderRadius, and boxShadow.
With scrub or showDuration, the video fills the width of its container, so set width to keep a smaller size.
The themed focus ring shows when the video has keyboard focus, including when a dialog focuses it on open. Without scrub or showDuration, the ring replaces any boxShadow you set while it shows.
TypeScript
HtVideoProps is a discriminated union with playback as its key. A plain Omit flattens the union, and drops the rules that tie each prop to its values. Derive a wrapper's props with a distributive Omit to keep them.
HtVideo passes through native <video> attributes that it does not own, such as crossOrigin, disablePictureInPicture, and disableRemotePlayback. It owns autoPlay, controls, muted, loop, and playsInline, so set those through playback and its props. It always sets playsInline, so the video plays inline on iPhone instead of opening fullscreen. See the video element on MDN for the full list.
Guidelines
When to use
- Show a video from a URL that is always a video, such as a product walkthrough or a splash page clip.
- Show a video thumbnail that the parent card opens on click, with
playback="none". - Preview a video thumbnail under the mouse, with
playback="hover"orplayback="scrub".
When not to use
- For a URL that can be an image or a video, such as an uploaded creative asset. Use HtMedia instead.
- For a player with custom controls, such as an ad preview with its own play and mute buttons. Render a
<video>and own its controls.
Accessibility
HtVideorequires eitheraltordecorative. Setaltwhen the video carries information, and say what it shows. It becomes the video'saria-label. An emptyaltnames a player with controls "Video", and hides a video without controls, so passdecorativeor real text instead.- Set
decorativefor an ambient clip that repeats nearby text. The video is hidden from screen readers, shows no controls, and shows its first frame unlessplaybackis set, such asautoplayfor an ambient loop. autoplayfollows the viewer's reduced motion setting when the video mounts, so do not add your own check. A change to the setting applies the next time the video mounts.- A looping video that carries content needs a way to pause it. Use
click, orautoplaywithcontrols. hoverandscrubrespond only to a mouse. Keyboard and touch users see the first frame, so do not put information only in the preview.- Add a captions
<track>when the video has speech.
Props
Inherits all style props, including color, border, and margin props, and the native attributes it does not own.
| Name | Default | Description |
|---|---|---|
style | — | ChakraBoxProps["style"]Inline styles, merged with the surface-derived style. |
showSkeleton | false | booleanShows a skeleton placeholder until the media loads. |
skeletonHeight | "200px" | BoxProps["height"]Height of the skeleton placeholder. |
showError | false | booleanReplaces the media with an error message if it fails to load. The message
takes the media's size, border radius, and margin props. |
onRetry | — | () => voidCalled from the Refresh button in the error message. The button shows only
when showError is set and this prop is passed. |
onLoad | — | BoxProps["onLoad"]Called when the media loads: when an image loads, or when a video has its first frame. |
onError | — | BoxProps["onError"]Called when the media fails to load. |
sx | — | BoxProps["sx"]Styles for the frame: the wrapper that scrub and showDuration add, or else the video. |
onPointerEnter | — | PointerEventHandler<HTMLElement>Runs before the hover and scrub handling, which still runs. |
onPointerMove | — | PointerEventHandler<HTMLElement>Runs before the scrub handling, which still runs. |
onPointerLeave | — | PointerEventHandler<HTMLElement>Runs before the hover and scrub handling, which still runs. |
src | — | stringURL of the video. Leave it out when children holds <source> elements. |
children | — | ReactNode<source> elements for other formats, and <track> elements for captions. |
poster | — | stringURL of an image to show before the video plays. |
preload | — | VideoHTMLAttributes<HTMLVideoElement>["preload"]Native hint for how much of the video to load before playback. |
loading | "eager" | "eager" | "lazy""lazy" waits to load the video until it comes within 200px of the viewport. Once loaded,
it stays loaded. |
alt | — | stringText alternative for the media. Required unless decorative is set. |
decorative | — | trueMarks the media as decorative, so screen readers skip it. |
playback | "click" | "click" | "none" | "autoplay" | "hover" | "scrub"How the video plays.
- "click": shows native controls, and the viewer starts playback.
- "none": shows the first frame, or poster when set, and never plays. The parent handles clicks.
- "autoplay": plays muted on a loop by default. Shows the first frame when the viewer prefers reduced motion.
- "hover": plays muted on a loop while a mouse is over the video, and returns to the first frame when it leaves. Shows the first frame, or poster, on touch and under reduced motion.
- "scrub": the mouse position across the video picks the frame, with a line under the cursor. Returns to the first frame when the mouse leaves. Shows the first frame, or poster, on touch. |
muted | — | booleanStarts the video muted. Defaults to true for autoplay, because browsers block autoplay with
sound, and to false for click. |
controlsList | — | VideoHTMLAttributes<HTMLVideoElement>["controlsList"]Native controlsList tokens that hide parts of the native controls: nodownload,
nofullscreen, noremoteplayback. Applies only while controls show. |
showDuration | false | booleanShows the video's length in a badge in the bottom corner, once it is known. Only for none,
hover, and scrub, which have no native timecode. |
controls | false | booleanShows native controls on an autoplaying video, so the viewer can pause it. Only for autoplay:
click always shows controls, and the other modes never do. |
loop | true | booleanRestarts the video when it ends. Only for autoplay. |
showScrubTime | false | booleanShows the badge with the time of the frame on screen while the viewer scrubs, and the length at rest.
Only for scrub. |