EmbedManager
A single consistent pattern for embedding YouTube, Vimeo, Spotify, GitHub Gists, Google Maps, and more. With lazy loading and privacy-respecting defaults built in.
The Problem
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.
Quick Start
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();
Intersection Observer Lazy Loading
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.
- ▸Default
rootMargin: '200px 0px'. Start loading before the element is visible - ▸Observer is disconnected once an embed loads. No unnecessary callbacks
- ▸Force immediate
load with
.processContainer(el)when scroll position doesn't matter
Privacy-Respecting Defaults
Privacy-friendly choices are made automatically. No configuration required.
- ▸YouTube. Uses
youtube-nocookie.com, no cookies unless user interacts - ▸Vimeo. Appends
dnt=1to opt out of Vimeo's tracker - ▸Website iframes. Sandboxed with restrictive
sandboxattribute andreferrerpolicy: no-referrer-when-downgrade - ▸URL validation. Only
https:andhttp:accepted;javascript:anddata:URIs are rejected
Runtime & Dynamic Embeds
Not 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
Accessibility Built In
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).
Supported Platforms
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 |
Configuration
Global options via the constructor, per-embed options via
data-* attributes.
Constructor Options
| 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
});
Universal Data Attributes
| 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 |
Platform-Specific Attributes
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 |
API Reference
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.
Examples
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>
Get Started
~8.6 KB minified, zero dependencies. Drop in the CDN script or
npm install embed-manager.