social-embed

Privacy-first social content embed with click-to-activate. Supports Bluesky, Mastodon, X, Instagram, and YouTube via a provider registry.

Overview

A privacy-first social content embed component. Click-to-activate by default — no third-party scripts or network requests execute until the user clicks the embed card. Supports Bluesky, Mastodon, X, Instagram, and YouTube out of the box, with an extensible provider registry for custom platforms.

The inner fallback content (typically an <a>) renders without JavaScript, is indexable by search engines, and remains accessible to all users.

Bluesky

Auto-detected from bsky.app URLs. Uses oEmbed — no third-party script injected.

Mastodon

Auto-detected from @user/id URL patterns. Parses the instance hostname from the URL and fetches oEmbed from the correct server.

X (Twitter)

Auto-detected from x.com or twitter.com status URLs. Loads the X widgets script on activation.

YouTube

Automatically delegates to <youtube-player>. The YouTube provider sets delegatesActivation: true, so social-embed skips its own click gate — youtube-player handles its own facade pattern (thumbnail + play button). One click, not two.

Explicit Provider

Use provider to bypass auto-detection when the URL pattern is ambiguous.

Activation Modes

Control when the embed loads with activate.

ValueBehaviour
click (default)User must click the idle card to trigger loading
visibleLoads when scrolled into viewport (IntersectionObserver with 200px margin)
eagerLoads immediately on page load

Dark Theme

Pass theme="dark" to hint to providers that support themed embeds. The default auto reads prefers-color-scheme.

With Caption

Wrap in a <figure> with <figcaption> for semantic context.

Attributes

AttributeRequiredTypeDefaultDescription
urlYesstringURL of content to embed
providerstringExplicit provider key. If omitted, auto-detection runs.
themestringautolight, dark, or auto (reads prefers-color-scheme)
activatestringclickclick, visible, or eager

Component-Managed Attributes

AttributeValuesDescription
stateidle, loading, loaded, error, unsupportedCurrent lifecycle state (read-only)

States

StateWhenVisual
idleBefore activation triggerBordered card with pointer cursor
loadingProvider render() calledSkeleton pulse animation
loadedrender() resolvedEmbed content displayed
errorrender() rejectedError border, fallback link restored
unsupportedNo provider matchedFallback link only (no error treatment)

Built-in Providers

KeyMechanismNotes
blueskyoEmbed fetchNo third-party script. Most private option.
mastodonoEmbed fetchInstance URL parsed automatically.
xScript injectionLoads platform.x.com/widgets.js. Respects theme.
instagramScript injectionLoads instagram.com/embed.js.
youtubeDelegates to <youtube-player>No double click — youtube-player handles its own facade.

Custom Provider

Register a custom provider with SocialEmbed.register():

Progressive Enhancement

ScenarioBehaviour
No JSFallback <a> link visible and clickable
JS + click-to-activateStyled card with pointer cursor, loads on click
JS + activate="visible"Loads when scrolled near viewport
JS + activate="eager"Loads immediately
Error during renderFallback content restored, error state styled

Accessibility

  • Fallback <a> provides a meaningful link for all users regardless of JS state
  • In idle state, the host has role="button" and tabindex="0" for keyboard access
  • Enter and Space keys activate the embed
  • Screen reader live region announces "Loading embed..." and "Embed failed to load." on state changes
  • Skeleton animation respects prefers-reduced-motion

Privacy

  • Click-to-activate by default — no third-party scripts or requests until user interaction
  • oEmbed providers (Bluesky, Mastodon) make one API request and inject HTML — no third-party scripts
  • Script-based providers (X, Instagram) inject platform scripts only after activation
  • YouTube delegates to <youtube-player> which uses youtube-nocookie.com
  • The component itself sets no cookies or localStorage

Related