A single consistent pattern for embedding YouTube, Vimeo, Spotify, GitHub Gists, Google Maps, and more — with lazy loading and privacy-respecting defaults built in.
Every platform has its own embed format. YouTube wants youtube-nocookie.com
for privacy. Twitch requires the current domain as a parent query
parameter. Twitter/X relies on a blockquote and an external widget script. GitHub Gists inject a
<script> tag. Getting all of these right — in a way
that respects privacy, handles errors gracefully, and doesn't tank page load — is tedious
boilerplate
to repeat across projects.
EmbedManager abstracts all of that into one consistent pattern: add a container with a few
data-* attributes, include the script, done.
Lazy by default
Iframes are injected only when the container scrolls near the viewport — via the Intersection Observer API.
Privacy-respecting
YouTube uses nocookie, Vimeo gets dnt=1, sandboxed iframes for arbitrary websites.
One pattern
12 supported platforms, all sharing the same
data-type / data-src attribute interface.
Two ways to include the library.
<!-- Drop this before </body> — no config needed -->
<script src="https://cdn.jsdelivr.net/npm/embed-manager/dist/embedManager.min.js"></script>
After the script loads, window.EmbedManager is
auto-initialized and observes all .embed-container elements on the page.
npm install embed-manager
import EmbedManager from 'embed-manager';
const mgr = new EmbedManager();
EmbedManager uses the IntersectionObserver API to watch every
.embed-container element. Iframes are injected only when the container
approaches the viewport — meaning network requests for content the user never scrolls
to are never made.
rootMargin: '200px 0px' — start loading before the element is
visible.processContainer(el) when scroll position doesn't matter
Privacy-friendly choices are made automatically — no configuration required.
youtube-nocookie.com, no
cookies unless user interactsdnt=1 to opt out of
Vimeo's trackersandbox attribute and
referrerpolicy: no-referrer-when-downgradehttps: and
http: accepted; javascript: and data: URIs are
rejectedNot everything is in the DOM on page load. EmbedManager handles embeds created and appended at runtime without reinitializing the whole instance.
const el = document.createElement('div');
el.className = 'embed-container';
el.setAttribute('data-type', 'youtube');
el.setAttribute('data-src', url);
document.body.appendChild(el);
mgr.addEmbed(el); // queued for lazy load
Every injected iframe receives title and aria-label attributes from
data-title. Error states use role="alert" and
aria-live="polite" — screen readers are notified when an embed fails to load.
Provide meaningful titles and they propagate through automatically. Omit them and the library falls back to Untitled Embed (still valid, but do provide them).
All 12 platform handlers share the same data-type /
data-src interface.
data-type |
Platform | Notes |
|---|---|---|
youtube |
YouTube | Auto-switches to youtube-nocookie.com |
vimeo |
Vimeo | dnt=1 applied by default; supports private/unlisted hash |
twitch |
Twitch | Auto-injects parent domain parameter |
codepen |
CodePen | Preview mode, themes, editable pens |
spotify |
Spotify | Tracks, albums, playlists, podcast episodes |
soundcloud |
SoundCloud | Customizable player color via data-color |
tiktok |
TikTok | Converts share URLs to embed format |
twitter or x |
Twitter / X | Transforms blockquote → widget; accepts Tweet IDs or URLs |
instagram |
Transforms blockquote → embed.js | |
gist or github |
GitHub Gists | Script-based embed wrapped in srcdoc iframe |
maps |
Google Maps | Requires data-api-key |
website |
Any URL | Sandboxed iframe, restricted sandbox + referrerpolicy |
Global options via the constructor, per-embed options via
data-* attributes.
| Option | Type | Default | Description |
|---|---|---|---|
rootMargin |
string |
'200px 0px' |
IntersectionObserver root margin — how early to start loading |
embedTimeout |
number |
15000 |
Timeout in ms for script-based embeds (Twitter, Instagram, Gist) |
const mgr = new EmbedManager({
rootMargin: '200px 0px', // load 200px before entering viewport
embedTimeout: 15000 // ms before script embeds time out
});
| Attribute | Required | Default | Description |
|---|---|---|---|
data-type |
yes | — | Platform identifier |
data-src |
yes | — | Source URL or content ID |
data-title |
no | 'Untitled Embed' |
Sets iframe title and aria-label |
data-width |
no | 100% |
Container width |
data-height |
no | — | Explicit height; disables aspect-ratio |
data-aspect-ratio |
no | 16/9 |
CSS aspect-ratio when no height is set |
CodePen
| Attribute | Default | Description |
|---|---|---|
data-default-tab |
result |
Tab to show: html, css, js,
result |
data-theme-id |
— | CodePen theme ID |
data-editable |
— | Set to "true" for editable pen |
Vimeo
| Attribute | Description |
|---|---|
data-hash |
Privacy hash for unlisted/private videos |
data-autoplay |
Set to "true" to autoplay |
data-app-id |
Vimeo app_id parameter |
Twitter / X
| Attribute | Default | Description |
|---|---|---|
data-theme |
light |
light or dark |
data-lang |
en |
Language code for embed UI |
SoundCloud
| Attribute | Default | Description |
|---|---|---|
data-color |
ff5500 |
Player color hex (no #) |
data-show-comments |
— | Set to "true" to show comments |
new EmbedManager(options?)
Creates a new instance and begins observing
all .embed-container elements in the DOM.
mgr.processContainer(container)
Immediately injects the iframe into a container, bypassing the IntersectionObserver. Use when scroll position doesn't apply.
const container = document.querySelector('#my-embed');
mgr.processContainer(container);
mgr.addEmbed(container)
Adds a dynamically created container to the lazy-load queue. Use after appending new embed containers to the DOM at runtime.
const el = document.createElement('div');
el.className = 'embed-container';
el.setAttribute('data-type', 'youtube');
el.setAttribute('data-src', 'https://www.youtube.com/watch?v=xyz');
document.body.appendChild(el);
mgr.addEmbed(el);
mgr.isValidUrl(url)
Returns true if the URL uses
https: or http:. Blocks javascript: and
data: URIs. Primarily internal, but available publicly.
Common patterns across the supported platforms.
YouTube
<div class="embed-container"
data-type="youtube"
data-src="https://www.youtube.com/watch?v=dQw4w9WgXcQ"
data-title="Never Gonna Give You Up">
</div>
Spotify Track
<div class="embed-container"
data-type="spotify"
data-src="https://open.spotify.com/track/4cO..."
data-aspect-ratio="unset"
data-height="152px"
data-title="Track Title">
</div>
Editable CodePen
<div class="embed-container"
data-type="codepen"
data-src="https://codepen.io/user/pen/abc"
data-default-tab="html,result"
data-editable="true"
data-title="My Demo">
</div>
GitHub Gist
<div class="embed-container"
data-type="gist"
data-src="https://gist.github.com/user/abc123"
data-title="Example Gist">
</div>
Vimeo — Private Video
<div class="embed-container"
data-type="vimeo"
data-src="https://vimeo.com/123456789"
data-hash="myPrivateHash"
data-title="Private Reel">
</div>
Square Aspect Ratio
<div class="embed-container"
data-type="vimeo"
data-src="https://vimeo.com/123456789"
data-aspect-ratio="1/1"
data-title="Square video">
</div>
~8.6 KB minified, zero dependencies. Drop in the CDN script or
npm install embed-manager.