A docs page is the hard case for embedding a clip#
A landing page carries one hero video: watched once, above the fold, on a page built around that single moment. A documentation page is the opposite environment, and every embedding habit that works on the landing page has to be re-checked against four things the docs page does differently.
It carries many clips, not one. A six-step how-to guide wants six short clips, and a careless embed multiplies its cost by six on one scroll. It gets read mid-task, usually by someone who did not come to watch anything and will leave if the page stalls. It gets exported: docs frameworks build offline bundles and PDFs, and a third-party <iframe> that plays fine online is a blank rectangle in the offline copy. And it outlives its framework. Docs get migrated between Docusaurus, MkDocs, and plain HTML more often than a marketing site gets rebuilt, and an embed written as a framework-specific component moves with far more friction than one written as portable markup.
So the real question is not how to get a video onto the page, which is trivial, but how to get several clips onto a page that still passes Core Web Vitals, still works offline, and still renders after the next migration. The pillar on where video earns its keep in docs settles which passages deserve a clip at all; this is the mechanics for the ones that do. Whether the clip should be a GIF instead and how small to cut it are separate calls; assume here you already have a short muted clip and need it on the page.
Three ways to put a clip on a docs page, ranked for docs#
| Approach | Portable across frameworks | Bytes on page load | Works offline / in PDF | Written in plain Markdown |
|---|---|---|---|---|
Self-hosted <video> |
Yes, raw HTML renders everywhere | Poster only, with preload="none" |
Yes, the file ships with the docs | Yes |
YouTube / Vimeo <iframe> |
Renders anywhere online, breaks offline | ~540 KB player unless deferred | No, needs the network | Yes, the raw iframe |
| Animated GIF | Yes, it is an <img> |
Largest of the three | Yes | Yes |
For a docs page the self-hosted <video> usually wins, because it is the only row that is portable, offline-safe, and light on load at the same time. What it trades away, adaptive bitrate and the egress bill, barely bites here: docs clips are short and the reader is usually on a working connection, so one modest file suits everyone. The full self-host versus YouTube versus streaming-host cost arithmetic is worked out for the marketing case elsewhere; the docs twist is that the two columns marketing can ignore, offline export and portability, are the ones that decide it here. Keep the YouTube iframe for the rare docs page whose point is reach, and defer it. Keep the GIF for the surface that genuinely refuses a <video> tag, a README the usual example, and pay its size and accessibility tax on purpose.
One codec note, then the syntax: give the <video> two sources, WebM first for smaller bytes and MP4 as the universal fallback, and let the browser take the first it can play. The container-versus-codec decision is a spoke of its own.
The exact embed, framework by framework#
Plain HTML is the portable floor. Most generators pass raw HTML through untouched, so this block renders in Docusaurus, MkDocs Material, GitHub-flavored Markdown that allows HTML, and a hand-written page alike:
<video controls preload="none" width="720" height="450"
poster="/img/export-step.png">
<source src="/vid/export-step.webm" type="video/webm">
<source src="/vid/export-step.mp4" type="video/mp4">
<a href="/vid/export-step.mp4">Download the clip</a> if it will not play inline.
</video>
Every attribute there earns its place, and preload and poster get their own section next.
Docusaurus (MDX). MDX is JSX, so raw HTML mostly works, with two catches: void elements must be self-closed (<source ... />), and no tag can be left unbalanced. Put the file in static/ and reference it with an absolute path, which Docusaurus resolves from the static directory, or wrap the path in require() so the file's "name will be appended by a hash, which enables browsers to aggressively cache" it1:
<video controls preload="none" width="720"
poster={require('./img/export-step.png').default}>
<source src={require('./vid/export-step.mp4').default} type="video/mp4" />
</video>
The temptation in Docusaurus is to wrap this in a custom <VideoPlayer> MDX component and reuse it everywhere. It is tidy, and it welds your content to Docusaurus; the last section is why that is a trap.
MkDocs Material. Material ships no video component of its own. Tellingly, its maintainer embeds the project's own demo from a third-party host, Streamable, with a raw <iframe>2. For a self-hosted clip you have two clean routes. Write the same HTML5 <video> block straight into the Markdown, since Material renders inline HTML. Or add the mkdocs-video plugin and embed with image syntax:

plugins:
- mkdocs-video:
is_video: true
video_muted: true
video_controls: true
Here is_video: True selects the <video> tag over an <iframe>, and video_muted, video_controls, video_autoplay, and video_loop map to the matching attributes3. The plugin buys cleaner Markdown; the raw block buys zero dependencies and identical output in every other framework.
Keeping a video-heavy page inside Core Web Vitals#
The metric that decides whether a docs page feels broken is Largest Contentful Paint, and Google's bar for "good" is "2.5 seconds or less for at least 75% of page visits"7. A page with six clips can miss it on one wrong attribute, so three settings carry the weight.
Defer the bytes, but use the right lever, because the two embed types do not share one. An <iframe>, a YouTube embed, takes loading="lazy", which defers the frame "until it reaches a calculated distance from the visual viewport" and cuts "initial page load times"5. A <video> element has no loading attribute at all. Its lever is preload, set to none to "prevent browsers from preloading any video data, even when in the viewport"6; web.dev calls that "the best chance of avoiding loading the video." Copying loading="lazy" onto a <video> does nothing, and that mismatched attribute is the single most common docs-embed bug.
With preload="none" the only byte a clip fetches before a click is its poster, "a placeholder that will occupy the space while the video loads"4. That makes the poster the asset to optimize, with one subtlety: a clip above the fold can make its poster the page's LCP element, and web.dev's rule is to "never lazy-load your LCP image"7. Compress the poster hard, and lazy-load the posters further down the page, not the one that paints first.
Reserve the space. Set explicit width and height, or a CSS aspect-ratio, on every embed so the browser lays out the box before the media arrives. Skip it and each clip that loads shoves the paragraph beneath it downward, the layout jump that Cumulative Layout Shift scores and a reader feels as text sliding out from under them mid-sentence. Reserved dimensions cost nothing and remove the shift completely.
The embed that survives a docs migration#
Docs get moved. A team on MkDocs adopts Docusaurus for versioned releases; a Docusaurus site collapses back to plain Markdown on a new host. The embeds that survive are the ones written as portable markup; the ones that fight you are the framework-specific conveniences. A raw <video> block is the same nine lines in MkDocs, Docusaurus, GitHub, and a bare HTML page. A custom <VideoPlayer> MDX component is a rewrite the day you leave Docusaurus, and a mkdocs-video marker is a find-and-replace the day you leave MkDocs. Convenience that reads well in one framework is debt in the next, so the portable block is the default and the plugin the deliberate upgrade.
Self-hosting is the portability choice for the same reason. A file shipped alongside the docs travels into the offline bundle, the PDF export, and the migrated site; a third-party <iframe> is a live network dependency that a reader offline, a printed PDF, or a locked-down enterprise mirror all render as a hole. The clip you own is the clip that still plays where you cannot reach.
That leaves the one problem no embed setting fixes: the clip goes stale. A docs video shows the exact interface a reader is about to touch, so the day the UI ships, every embed pointing at the old screen is quietly wrong, and a fast, offline-safe, perfectly portable embed of last quarter's UI is still a lie in motion. The fix that scales is to rebuild the clip from a spec on the same commit that changed the screen, the maintenance model that keeps a tutorial library current. aidemo, the open-source engine we build, is one way to do it: an agent writes a storyboard and a deterministic run re-records the clip, so the embedded file is regenerated on the release that changed the UI, not reshot by hand. The honest bounds: it captures a browser and nothing outside one, the storyboard is agent-drafted and then edited as text, and there is no drag-and-drop timeline anywhere. Getting the clip onto the page is the easy half. Keeping it true to the product is the half that decides whether the page is documentation or decoration.
Sources#
- Docusaurus — Markdown Features: Assets
- Material for MkDocs — How to embed videos (discussion #3984)
- mkdocs-video — plugin README (Markdown syntax and config)
- MDN — The Video Embed element (preload, poster, dimensions)
- MDN — The Inline Frame element (loading attribute)
- web.dev — Lazy-loading video
- web.dev — Optimize Largest Contentful Paint
FAQ#
How do I embed a local MP4 video in Docusaurus or MkDocs?#
In both, the portable way is a raw HTML5 <video> block written straight into the Markdown, since both render inline HTML: point a <source> at the file and add controls and preload="none". In Docusaurus (MDX) put the file in static/ and self-close the <source /> tag, or wrap the path in require() for hash-based caching. In MkDocs Material you can instead add the mkdocs-video plugin and write  with is_video: True in the config.
Can I lazy-load a self-hosted video on a documentation page?#
Not with loading="lazy" — that attribute works on <img> and <iframe> elements, not on <video>. To defer a self-hosted clip, set preload="none", which web.dev calls the best chance of avoiding loading the video before a click; the only thing that then loads is the poster image. Use loading="lazy" only on an iframe embed such as YouTube, and never on the poster or clip that is your Largest Contentful Paint element.
Will a documentation page with several video clips fail Core Web Vitals?#
Only if you let the clips load eagerly. Set preload="none" on every self-hosted <video> so nothing but the poster loads up front, give each embed explicit width and height (or aspect-ratio) to prevent layout shift, and defer any YouTube iframes with loading="lazy". With those in place, the bytes that count against Largest Contentful Paint are a handful of compressed posters, and the page clears the 2.5-second "good" threshold with room to spare.



