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.
<social-embed url="https://bsky.app/profile/bsky.app/post/3lb55bvibcs2w"> <a href="https://bsky.app/profile/bsky.app/post/3lb55bvibcs2w">View post on Bluesky</a></social-embed>
Mastodon
Auto-detected from @user/id URL patterns. Parses the instance hostname from the URL and fetches oEmbed from the correct server.
<social-embed url="https://mastodon.social/@Mastodon/109838793196353548"> <a href="https://mastodon.social/@Mastodon/109838793196353548">View post on Mastodon</a></social-embed>
X (Twitter)
Auto-detected from x.com or twitter.com status URLs. Loads the X widgets script on activation.
<social-embed url="https://x.com/github/status/1234567890"> <a href="https://x.com/github/status/1234567890">View post on X</a></social-embed>
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.
<social-embed url="https://www.youtube.com/watch?v=dQw4w9WgXcQ"> <a href="https://www.youtube.com/watch?v=dQw4w9WgXcQ">Watch on YouTube</a></social-embed>
Explicit Provider
Use provider to bypass auto-detection when the URL pattern is ambiguous.
<social-embed provider="mastodon" url="https://mastodon.social/@user/12345"> <a href="https://mastodon.social/@user/12345">View post on Mastodon</a></social-embed>
Activation Modes
Control when the embed loads with activate.
| Value | Behaviour |
|---|---|
click (default) | User must click the idle card to trigger loading |
visible | Loads when scrolled into viewport (IntersectionObserver with 200px margin) |
eager | Loads immediately on page load |
<social-embed url="https://bsky.app/profile/user/post/3abc" activate="visible"> <a href="https://bsky.app/profile/user/post/3abc">View post on Bluesky</a></social-embed>
Dark Theme
Pass theme="dark" to hint to providers that support themed embeds. The default auto reads prefers-color-scheme.
<social-embed url="https://x.com/user/status/123" theme="dark"> <a href="https://x.com/user/status/123">View post on X</a></social-embed>
With Caption
Wrap in a <figure> with <figcaption> for semantic context.
<figure> <social-embed url="https://bsky.app/profile/user/post/3abc"> <a href="https://bsky.app/profile/user/post/3abc">View post on Bluesky</a> </social-embed> <figcaption>A post about the new CSS features.</figcaption></figure>
Attributes
| Attribute | Required | Type | Default | Description |
|---|---|---|---|---|
url | Yes | string | — | URL of content to embed |
provider | — | string | — | Explicit provider key. If omitted, auto-detection runs. |
theme | — | string | auto | light, dark, or auto (reads prefers-color-scheme) |
activate | — | string | click | click, visible, or eager |
Component-Managed Attributes
| Attribute | Values | Description |
|---|---|---|
state | idle, loading, loaded, error, unsupported | Current lifecycle state (read-only) |
States
| State | When | Visual |
|---|---|---|
idle | Before activation trigger | Bordered card with pointer cursor |
loading | Provider render() called | Skeleton pulse animation |
loaded | render() resolved | Embed content displayed |
error | render() rejected | Error border, fallback link restored |
unsupported | No provider matched | Fallback link only (no error treatment) |
Built-in Providers
| Key | Mechanism | Notes |
|---|---|---|
bluesky | oEmbed fetch | No third-party script. Most private option. |
mastodon | oEmbed fetch | Instance URL parsed automatically. |
x | Script injection | Loads platform.x.com/widgets.js. Respects theme. |
instagram | Script injection | Loads instagram.com/embed.js. |
youtube | Delegates to <youtube-player> | No double click — youtube-player handles its own facade. |
Custom Provider
Register a custom provider with SocialEmbed.register():
import { SocialEmbed, loadScript, fetchOEmbed } from './social-embed/logic.js'; SocialEmbed.register('my-platform', { canHandle(url) { return url.includes('my-platform.example'); }, async render(host, url, { theme }) { const data = await fetchOEmbed('https://my-platform.example/oembed', url); host.innerHTML = data.html; }});
Progressive Enhancement
| Scenario | Behaviour |
|---|---|
| No JS | Fallback <a> link visible and clickable |
| JS + click-to-activate | Styled card with pointer cursor, loads on click |
JS + activate="visible" | Loads when scrolled near viewport |
JS + activate="eager" | Loads immediately |
| Error during render | Fallback 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"andtabindex="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 usesyoutube-nocookie.com - The component itself sets no cookies or localStorage
Related
<youtube-player>— Standalone YouTube embed with facade pattern<share-wc>— Social sharing buttons