Lazy Loading Privacy-First Zero Dependencies MIT ~8.6 KB

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.

CDN npm
<!-- 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.

CDN npm
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=1 to opt out of Vimeo's tracker
  • Website iframes — sandboxed with restrictive sandbox attribute and referrerpolicy: no-referrer-when-downgrade
  • URL validation — only https: and http: accepted; javascript: and data: 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.

youtube vimeo twitch codepen spotify soundcloud tiktok twitter / x instagram gist / github maps website
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 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.