Lazy YouTube, Vimeo, Cloudflare Stream, and Matterport embeds that load nothing until clicked
Tags: component media display
MediaEmbed shows a poster image and a play button, and only injects the provider's iframe once the viewer clicks it. Until then the page loads none of the provider's player code, which keeps it light and sets no tracking cookies on arrival.
When the poster is derived for YouTube, the image itself is still fetched from i.ytimg.com — a static image host, not the player. Pass your own poster to avoid contacting the provider at all before playback.
Import
<script>
import { MediaEmbed } from '@classic-homes/theme-svelte';
</script>Basic Usage
<MediaEmbed url="https://youtu.be/dQw4w9WgXcQ" title="Never Gonna Give You Up" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
url |
string |
- | Full provider URL; ignored when provider and videoId are set |
provider |
'youtube' | 'vimeo' | 'cloudflare' | 'matterport' |
- | Hosting provider |
videoId |
string |
- | Provider-specific video identifier |
title |
string |
required | Accessible name for the player; required |
poster |
string |
- | Poster image; derived for YouTube and Stream, required for Vimeo |
privacy |
boolean |
true |
Route YouTube through the no-cookie domain |
start |
number |
- | Start offset in seconds |
showRelated |
boolean |
false |
Show the provider's related-video suggestions |
customerCode |
string |
- | Cloudflare Stream customer code; required for that provider |
domain |
string |
- | Custom hostname serving Cloudflare Stream |
primaryColor |
string |
- | Accent colour for the Cloudflare player UI |
letterboxColor |
string |
- | Letterbox colour for the Cloudflare player |
defaultTextTrack |
string |
- | BCP-47 code of the captions track the Cloudflare player enables |
modelId |
string |
- | Matterport space id; an alias for videoId |
matterport |
MatterportPlayerOptions |
- | Showcase options for the Matterport player |
fetchMetadata |
boolean |
false |
Look up the space's preview image and name from Matterport |
ratio |
'square' | 'video' | 'photo' | 'portrait' | 'wide' | number |
'video' |
Aspect ratio of the embed box |
eager |
boolean |
false |
Load the iframe immediately instead of showing a facade |
class |
string |
- | Additional CSS classes |
Supported URL Formats
url accepts every common share and watch link, including the start offset:
https://www.youtube.com/watch?v=VIDEO_ID
https://youtu.be/VIDEO_ID?t=30
https://www.youtube.com/embed/VIDEO_ID
https://www.youtube.com/shorts/VIDEO_ID
https://www.youtube.com/live/VIDEO_ID
https://vimeo.com/VIDEO_ID
https://player.vimeo.com/video/VIDEO_ID
https://customer-CODE.cloudflarestream.com/VIDEO_UID/iframe
https://customer-CODE.cloudflarestream.com/VIDEO_UID/watch
https://iframe.videodelivery.net/VIDEO_UID
https://my.matterport.com/show/?m=MODEL_IDAn unrecognised URL renders an inline error rather than an empty frame, and warns in development.
Cloudflare Stream
Cloudflare Stream needs the account as well as the video, either as customerCode or as part of a full URL:
<MediaEmbed
provider="cloudflare"
videoId="a1b2c3d4e5f6"
customerCode="abc123"
title="Product tour"
primaryColor="#c8102e"
/>The poster is derived from Stream's URL scheme, so the facade needs no poster and no API call. A signed playback token works in place of the video id for private videos.
This embeds Cloudflare's own player, which brings its own controls, adaptive bitrate, and analytics. To play a Stream video with this design system's controls instead, use Video with cloudflareStream().
Matterport 3D Tours
Matterport embeds an interactive 3D space rather than a video, so it has no timeline: tour tiles show a 3D affordance instead of a play button, and never a duration.
<MediaEmbed
provider="matterport"
modelId="SxQL3iGyoDo"
title="Show home tour"
matterport={{ brand: false, highlightReel: false }}
/>A full showcase URL works too, and the space id is read from it:
<MediaEmbed url="https://my.matterport.com/show/?m=SxQL3iGyoDo" title="Show home tour" />For galleries, matterportTour() builds an item that MediaGrid, MediaCarousel, and Lightbox all understand:
<script lang="ts">
import { MediaGrid, matterportTour } from '@classic-homes/theme-svelte';
const tours = [matterportTour('SxQL3iGyoDo', { title: 'Show home', caption: 'Ground floor' })];
</script>
<MediaGrid items={tours} label="Property tours" />Showcase options
Passed as matterport={{ … }}. Matterport's own defaults are left alone unless an option asks otherwise, so a tour looks the way its owner configured it.
| Option | Type | Default | Showcase parameter |
|---|---|---|---|
quickStart |
boolean |
true |
qs — reach an explorable view sooner |
autoplay |
boolean |
false |
play — start moving on load |
brand |
boolean |
true |
brand — "Presented by" and contact details |
help |
boolean |
true |
help — in-player help button |
highlightReel |
boolean |
true |
hr — highlight reel along the bottom |
vr |
boolean |
true |
vr — VR button |
floorSelector |
boolean |
true |
f — floor selector |
mls |
boolean |
false |
mls — MLS-compliant presentation |
lang |
string |
- | lang — interface language |
guidedTourStart |
number |
- | ts — seconds before the guided tour starts |
params |
object |
- | Any further showcase parameter, verbatim |
quickStart defaults to on because without it the space shows a black frame for several seconds while the first view resolves.
Tour thumbnails
Matterport publishes no oEmbed endpoint and no derivable thumbnail URL, so a tour facade shows a deliberate placeholder unless you give it one:
<MediaEmbed provider="matterport" modelId="SxQL3iGyoDo" title="Show home" poster="/tour.jpg" />fetchMetadata opts into a runtime lookup of the space's real preview image and name. It is off by default, because contacting the provider before the viewer clicks is the very thing the lazy facade avoids. The image URL Matterport returns is signed and time-limited, so it cannot be cached or precomputed.
<MediaEmbed provider="matterport" modelId="SxQL3iGyoDo" title="Show home" fetchMetadata />The lookup never runs when poster is set, and any failure falls back to the placeholder rather than breaking the render.
Only public spaces are supported. Access-restricted models need an OAuth flow and an application key, which this component does not do.
Vimeo Posters
YouTube thumbnails follow a predictable URL, so MediaEmbed derives them. Vimeo thumbnails require an oEmbed lookup, which would defeat the point of a facade — supply poster instead:
<MediaEmbed url="https://vimeo.com/123456789" title="Design process" poster="/vimeo-poster.jpg" />Explicit Provider and ID
<MediaEmbed provider="youtube" videoId="dQw4w9WgXcQ" title="A song" start={30} />Accessibility
titleis required and becomes the iframe's accessible name. Without it, screen readers announce only "frame".- The facade is a real button, so it is keyboard reachable and activates with
EnterorSpace. - Its label names both the media and the provider, so viewers know a third-party player is about to load. A Matterport facade is labelled "Open … as a 3D tour", since it opens a space rather than playing a video.
- The poster image is decorative and hidden from assistive technology.
Related Components
- Video - Self-hosted video with themed controls
- AspectRatio - The ratio box the embed uses
- MediaCarousel - Embeds among other media types