Embed Widgets & oEmbed Implementation
If you're building a B2B SaaS in 2026 with content users want to share — forms (Tally / Typeform), schedulers (Calendly), videos (Loom), boards (Trello), polls (Mentimeter), data viz (Datawrapper) — letting users embed your product on their site is a powerful growth loop. Embeds drive: brand exposure to embedder's audience, viral discovery, ease of use for end-users (don't need account). The naive approach: provide a script tag and call it done. The structured approach: choose iframe vs JS widget, secure the embed (CSP, sandboxing, postMessage), implement oEmbed for rich-link unfurls (Notion, Slack, Twitter), responsive sizing, theming, accessibility. (See markdown-rendering-sanitization-chat for inverse — rendering external embeds inside your product.)
1. Decide embed type — iframe vs JS widget vs oEmbed
Pick embed type.
iframe (most common):
- Simple HTML: <iframe src="https://yourapp.com/embed/abc">
- Cross-origin isolation
- Full control over content
- Pro: secure (sandboxed); easy to implement
- Con: rigid sizing; less interactive
JS widget (richer):
- <script src="https://yourapp.com/widget.js"></script>
- Renders into <div data-widget-id="abc">
- Pro: dynamic; interactive; theming
- Con: more complex; security surface; CORS
oEmbed (rich-link unfurls):
- Spec for converting URL → embed code
- Used by: Twitter, YouTube, Spotify, Notion
- When user pastes link, host fetches /oembed?url=... → returns HTML
- Pro: works in many platforms automatically
- Con: spec-bound; less customizable
Combinations:
- iframe + oEmbed: most common for content
- JS widget + oEmbed: for interactive
- All three: full coverage
For [PRODUCT], decision factors:
Form / scheduler / poll → iframe + oEmbed
Video / interactive widget → iframe + JS widget hybrid
Live data / dashboards → JS widget for richness
Social platform compatibility → oEmbed required
Output:
1. Recommendation
2. iframe vs JS for primary
3. oEmbed support
4. Browser compatibility
5. Mobile considerations
The 2026 default for most B2B SaaS embeds: iframe + oEmbed. Iframe gives security; oEmbed gives social-platform compatibility.
2. Build the iframe embed
Implement iframe embed.
URL pattern:
- /embed/[id] (clean)
- Or: /embed?id=[id] (legacy)
- Public URL (no auth required usually)
Embed page setup:
Minimal layout (no nav / footer):
- Strip product chrome
- Just the content
- Branding minimal (small "Powered by [Product]" or hidden)
Responsive:
- viewport meta tag
- Max-width constraint
- height: auto
- Or: send postMessage on resize
Embed snippet:
<iframe
src="https://yourapp.com/embed/abc123"
width="100%"
height="600"
frameborder="0"
allow="..." (camera, microphone, etc. if needed)
sandbox="allow-scripts allow-forms allow-same-origin"
title="Form by Yourapp"
></iframe>
Sandbox attributes:
- allow-scripts (run JS)
- allow-forms (submit forms)
- allow-same-origin (cookies / localStorage)
- allow-popups (open new window)
- Don't allow: allow-top-navigation (security)
Cross-frame communication:
postMessage for resize:
- Embed sends current height to parent
- Parent listens; resizes iframe
window.parent.postMessage({
type: 'resize',
height: document.body.scrollHeight
}, '*');
Parent:
window.addEventListener('message', (e) => {
if (e.origin !== 'https://yourapp.com') return; // SECURITY
if (e.data.type === 'resize') {
iframe.style.height = e.data.height + 'px';
}
});
Output:
1. Embed URL pattern
2. Embed-page layout
3. Sandbox config
4. PostMessage protocol
5. Test on different host sites
The postMessage origin check: critical for security. Without it, any site can spoof messages from your embed. Always verify origin.
3. Build JS widget embed
Implement JS widget embed.
Snippet:
<script src="https://yourapp.com/widget.js" async></script>
<div data-widget="abc123"></div>
Widget script (widget.js):
(function() {
// 1. Find all widget targets
const widgets = document.querySelectorAll('[data-widget]');
widgets.forEach(el => {
const id = el.dataset.widget;
// 2. Create iframe inside (or render React)
const iframe = document.createElement('iframe');
iframe.src = `https://yourapp.com/embed/${id}`;
iframe.style.width = '100%';
iframe.style.border = 'none';
el.appendChild(iframe);
// 3. Listen for resize messages
window.addEventListener('message', (e) => {
if (e.origin !== 'https://yourapp.com') return;
if (e.data.widgetId === id && e.data.type === 'resize') {
iframe.style.height = e.data.height + 'px';
}
});
});
})();
Why JS over iframe-direct:
- Auto-resize (script handles)
- Multiple widgets per page
- Custom config via data attributes
- Theme inheritance (read CSS vars from parent)
Theme inheritance:
- Read computed colors from parent
- Pass via URL params to iframe
- Iframe applies theme
Cleanup:
- Widget can be removed from DOM
- Listener cleanup needed
- MutationObserver to detect removal
Hosting:
- CDN (Cloudflare / Vercel)
- Versioning: yourapp.com/widget/v1.js
- Cache headers (long for versioned; short for latest)
- Subresource Integrity (SRI) hash for security
Output:
1. Widget script architecture
2. Init flow
3. Theme handling
4. Lifecycle (init / cleanup)
5. CDN + versioning
The Calendly widget pattern: tiny JS bootstrap that injects iframe. Combines iframe security with JS flexibility.
4. oEmbed — rich-link unfurls
oEmbed = "when someone pastes a link to your product into Twitter/Notion/Slack, it auto-embeds."
Implement oEmbed.
Spec basics:
When third-party platform sees a URL, they request:
GET https://yourapp.com/oembed?url=https://yourapp.com/embed/abc&format=json
You respond:
{
"type": "rich",
"version": "1.0",
"title": "Form Title",
"html": "<iframe src='...' width='600' height='400'></iframe>",
"width": 600,
"height": 400,
"provider_name": "Yourapp",
"provider_url": "https://yourapp.com"
}
oEmbed types:
- photo (image)
- video (player)
- link (just metadata; no embed)
- rich (HTML embed)
Discovery (auto-detect):
Add to your embed page <head>:
<link rel="alternate" type="application/json+oembed" href="https://yourapp.com/oembed?url=...&format=json">
<link rel="alternate" type="text/xml+oembed" href="https://yourapp.com/oembed?url=...&format=xml">
Now Notion / Slack / Twitter auto-discover oEmbed URL.
Implementation:
Endpoint: /oembed?url=...
- Validate URL belongs to you
- Fetch resource info
- Return JSON / XML
Allowlist:
- Only embed your-domain URLs
- Validate input (don't embed arbitrary URLs)
Cache:
- Long cache (oEmbed responses don't change often)
- Edge-cached via CDN
OEmbed providers list:
- Some platforms maintain list of oEmbed providers
- Submit to Embed.ly to be auto-recognized
- Otherwise: only platforms that auto-discover
Output:
1. /oembed endpoint
2. Discovery <link> tags
3. Validation
4. Caching
5. Provider listing submission
The "pasting URL becomes embed in Notion" magic: oEmbed makes that work. ~30 lines of code; huge UX win.
5. Embed customization — theming + branding
Embedders want to match their site's look.
Allow customization.
URL parameters:
/embed/abc?theme=dark
/embed/abc?primary=#3366ff
/embed/abc?layout=compact
Common params:
- theme (light / dark / auto)
- primary color
- accent color
- font (system / serif / mono)
- layout (compact / spacious)
- branding (show / hide)
Theme handling:
Auto-detect parent:
- Read prefers-color-scheme
- Read parent CSS vars (limited cross-origin)
- Default to light
Custom CSS via URL params:
- Allow hex colors only (validate)
- Don't allow arbitrary CSS (XSS risk)
- Sanitize inputs
Branding:
Default: visible "Powered by [Product]"
- Free tier: required
- Paid tier: optional / removable
Removal:
- Higher tier or paid customer
- Validate via API key in embed URL
Pricing:
- Free embeds: branded; rate-limited
- Paid: unbranded; high-volume; custom domain
Custom domain (white-label):
embed.customer.com → CNAME to your service
- Customer's branding
- SSL via Cloudflare / Vercel
- Validate domain ownership
Output:
1. Customization params
2. Theme matching
3. Branding control
4. Custom domain (if white-label)
5. Tier-based features
The "Powered by Yourapp" growth loop: every embed = brand exposure. 1000 embedded forms × 100 viewers each = 100K impressions. Worth more than ad spend; don't remove easily.
6. Security — sandbox, CSP, isolation
Embeds are a security surface. Lock it down.
Secure embeds.
Iframe sandbox:
Required:
- sandbox="allow-scripts allow-forms allow-same-origin"
- Restrict where possible
Don't allow:
- allow-top-navigation (escape iframe)
- allow-modals (popup attacks)
- allow-pointer-lock (UX abuse)
Content Security Policy (CSP):
In embed page:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
frame-ancestors *; (allow embedding)
Restrict frame-ancestors:
- 'none' = nobody can embed
- 'self' = only your domain
- '*' = anyone (typical for public embed)
- specific URLs = allowlist
X-Frame-Options:
- Deprecated; use CSP frame-ancestors
- For older browsers
PostMessage validation:
Always check e.origin:
- Don't trust messages from any origin
- Allowlist: ['https://yourapp.com', 'https://embed.yourapp.com']
- Reject mismatch
Authentication:
Public embeds:
- No auth required
- Public-readable resource
Private embeds:
- Token in URL
- Or: short-lived signed URL
- Auth via embedding party's session (postMessage to parent)
API rate limiting:
Per-domain:
- Track which sites embed you
- Rate-limit per embedding domain
- Detect abuse (1000 embeds from suspicious site)
Per-IP:
- Standard rate limiting
XSS prevention:
User-generated content in embed:
- Sanitize before display
- See markdown-rendering-sanitization-chat
Sensitive data:
- Don't embed forms with sensitive data on third-party sites
- Or: warn user
- Or: require auth
Output:
1. Sandbox config
2. CSP headers
3. PostMessage origin check
4. Auth strategy
5. Rate limiting
6. XSS mitigation
The frame-ancestors discipline: define who can embed you. "*" allows anyone (needed for public embeds); specific allowlist for private content.
7. Responsive embeds
Embeds need to work on mobile + various sizes.
Make embeds responsive.
Aspect ratio container:
CSS for parent (preferred):
.embed-container {
position: relative;
padding-bottom: 75%; /* aspect ratio */
height: 0;
}
.embed-container iframe {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
Auto-resize via postMessage:
- Embed measures content height
- Sends to parent
- Parent updates iframe height
Width:
- 100% of parent (most cases)
- Max-width for desktop (avoid stretching)
Mobile:
- Touch-friendly inputs (44px+ tap targets)
- Reduce padding
- Hide non-critical UI
- Test on mobile Safari + Chrome
Print:
- Often hidden
- Show "View online" placeholder
- Or: print-friendly CSS
Different aspect ratios:
Form: variable height (postMessage)
Video: 16:9 typical
Calendar: variable
Dashboard: variable
Output:
1. Aspect ratio CSS
2. PostMessage resize protocol
3. Mobile UX
4. Print handling
5. Test plan
The "embed too tall on mobile" problem: forms with many fields stretch on mobile. Solve with postMessage-based dynamic height; iframe grows as form completes.
8. Embed analytics
Track embed performance.
Track embed analytics.
Events to track:
Embed render:
- Where embedded (referrer)
- Embed ID (which resource)
- Timestamp
Embed interaction:
- View
- Submit / click / play
- Time spent
Tracking implementation:
Server-side (basic):
- Log embed page hits
- Referrer header tells you where embedded
- Group by domain → embedders dashboard
Client-side (richer):
- JS in embed sends events to your analytics
- See product analytics (PostHog / Amplitude)
- Privacy: GDPR / cookie compliance
Privacy considerations:
Cookies:
- Iframe cookies are third-party (limited by browsers)
- Use first-party server-side tracking
- Or: cookieless analytics
GDPR:
- Iframe inherits parent site's consent
- But your embed creates separate processing
- DPA with embedding parties (rare; usually privacy notice covers)
Anti-patterns:
- Tracking pixels without disclosure
- Cross-site fingerprinting
- Sharing data with embedders without consent
Embedder dashboard:
Show your customer:
- How many sites embed their content
- View / interaction rates
- Top embedding domains
- Conversion rates
This is differentiation: customers choose your embed because of insight.
Output:
1. Event tracking
2. Server vs client analytics
3. Privacy compliance
4. Embedder dashboard
5. Anti-pattern checklist
The "embed insights drive retention" insight: customers stay if your embed analytics show value. "Your form was viewed 1000 times across 5 sites" beats "Powered by [Product]."
9. Common products — patterns
Different SaaS types have different embed patterns.
Common embed patterns by product type.
Forms (Typeform, Tally, Google Forms):
- Iframe + responsive
- Auto-resize on field expansion
- Submit handled in iframe
- oEmbed for paste-as-link
- Examples: <iframe src="https://tally.so/forms/abc">
Schedulers (Calendly, Cal.com, SavvyCal):
- Iframe with calendar
- Inline + popup widget options
- Pre-fill via URL params
- Booking confirmation in-iframe
- oEmbed for social platforms
Video (Loom, Vimeo, Wistia):
- Iframe with video player
- Lazy-load (poster image until clicked)
- oEmbed required for Twitter / Notion / etc.
- Aspect-ratio responsive
Social embeds (Twitter, Reddit, Reddit):
- oEmbed primary (link → embed)
- Iframe + JS combination
- Tweet rendering shows author / engagement
Polls / surveys (Mentimeter, Polly):
- Real-time iframe
- WebSocket for live updates
- Respondent ID via cookie / fingerprint
Dashboards (Datawrapper, Mode, Geckoboard):
- JS widget for richness
- Theming critical
- Often interactive
E-commerce widgets (Stripe Checkout, Shopify Buy Button):
- iframe-popup for payment (security + PCI)
- "Buy" button → iframe overlay
Forms / quizzes / lead capture:
- iframe + tracking pixels
- Anti-bot (captcha)
- Privacy-respecting
For [PRODUCT TYPE], output:
1. Recommended pattern
2. Iframe vs JS vs oEmbed mix
3. Specific gotchas
4. Pre-fill / config options
5. Mobile / accessibility
The Calendly pattern: small JS bootstrap → injects iframe → handles resize + analytics. Standard for interactive widgets.
10. Distribution + adoption
Embeds drive growth if customers actually embed.
Drive embed adoption.
In-product affordance:
"Share" button on resource:
- Options: Copy link / Embed / Email / Social
- Embed → modal with snippet
Embed snippet:
- Code snippet (iframe / JS)
- Customization options (theme, size)
- Copy button
- Preview
Onboarding:
Suggest embed during onboarding:
- "Add to your site"
- Customer education on benefits
Customer-facing:
- Help docs: "How to embed"
- Per-platform guides (Squarespace, Webflow, WordPress, etc.)
- Video tutorials
Platform integrations:
WordPress plugin:
- One-click embed
- Easier than copy-paste
Webflow:
- Custom code embed
Notion:
- oEmbed auto-works
- Document the URL pattern
Growth loops:
Tier-based branding:
- Free: branded ("Powered by")
- Paid: unbranded
- Upgrade incentive
Embed analytics for embedders:
- Show value (views, conversions)
- Lock-in via insights
Anti-patterns:
- Hidden behind paywall (free tier should embed)
- Complex setup
- No customization (looks foreign)
- Heavy / slow (impacts embedder's site)
Output:
1. In-product share UI
2. Customer education
3. Platform integrations
4. Tier strategy
5. Performance optimization
The "embed must load fast" rule: iframe / JS that takes 5s to load slows embedder's site. Embedders cut you. Optimize bundle size + cache aggressively.
What Done Looks Like
A v1 embed system:
- iframe embed at /embed/[id]
- Sandbox + CSP configured
- PostMessage protocol with origin check
- Responsive (auto-resize)
- oEmbed endpoint at /oembed
- Discovery tags
- Theme customization (URL params)
- Branding (free) / unbranded (paid)
- Embed analytics tracked
- Embedder dashboard
- In-product "Embed" share button
- Help docs per platform
Add later when product is mature:
- JS widget for richer interaction
- Custom domain (white-label)
- Platform-specific plugins (WordPress, Webflow)
- Multi-embed analytics
- A/B test embed variants
- Embedder API
The mistake to avoid: no origin check on postMessage. Anyone can spoof; security hole.
The second mistake: embed too heavy. Embedders' sites slow down; cut you.
The third mistake: no oEmbed. Twitter / Notion / Slack pastes don't render; lose social platform reach.
See Also
- Markdown Rendering & Sanitization — render external embeds
- File Preview & Document Viewer — adjacent
- Multi-Tenancy — tenant scoping for embeds
- Customer Analytics Dashboards — embedder analytics
- Quotas, Limits & Plan Enforcement — branded-vs-unbranded tiers
- Performance Optimization — embed perf
- Captcha & Bot Protection — embed abuse
- Rate Limiting & Abuse — rate-limit
- Roles & Permissions — auth in embeds
- Cookie Consent — privacy
- VibeReference: CDN Providers — CDN for widget hosting
- VibeReference: Vercel Functions — embed endpoints
- VibeReference: Form Builders — Tally / Typeform examples
- VibeReference: Scheduling & Booking APIs — Calendly / Cal.com
- VibeReference: Video Hosting & Streaming Providers — Mux / Loom embeds
- LaunchWeek: Public Roadmap — embed roadmap example