apps/docs/content/sdk-features/embed-shape.mdx
The embed shape displays interactive content from external services within an iframe. When you paste a URL from a supported service onto the canvas, tldraw converts it to an embed with the appropriate dimensions and settings. See EmbedShapeUtil and TLEmbedShape.
Paste a supported URL onto the canvas, or create an embed shape programmatically:
editor.createShape({
type: 'embed',
x: 100,
y: 100,
props: {
url: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
w: 560,
h: 315,
},
})
The embed system recognizes URLs from supported services and converts them to their embeddable equivalents. A YouTube watch URL becomes an embed URL automatically.
You can also paste raw <iframe> HTML directly onto the canvas to create an embed shape, even when the source URL doesn't match a known provider. tldraw extracts the iframe's src attribute and creates an embed pointing at it. For size, it reads the width and height attributes, then pixel values from the style attribute, and otherwise uses 425×350.
This covers services tldraw doesn't know about (OpenStreetMap, SoundCloud, Loom, internal tools) without a custom embed definition. Iframes pasted this way receive a stricter sandbox than the built-in providers (unknownEmbedShapePermissionOverrides: no allow-same-origin, no forms, no popups), since tldraw can't make any safety guarantees about the source.
| Service | Hostnames | Resizable | Aspect ratio locked |
|---|---|---|---|
| tldraw | tldraw.com, beta.tldraw.com, localhost:3000 | Yes | No |
| Figma | figma.com | Yes | No |
| YouTube | youtube.com, *.youtube.com, youtu.be | Yes | Yes |
| Google Maps | google.* | Yes | No |
| Google Calendar | calendar.google.* | Yes | No |
| Google Slides | docs.google.* | Yes | No |
| CodeSandbox | codesandbox.io | Yes | No |
| CodePen | codepen.io | Yes | No |
| Scratch | scratch.mit.edu | No | No |
| Val Town | val.town | Yes | No |
| GitHub Gist | gist.github.com | Yes | No |
| Replit | replit.com | Yes | No |
| Felt | felt.com | Yes | No |
| Spotify | open.spotify.com | Yes | No |
| Vimeo | vimeo.com, player.vimeo.com | Yes | Yes |
| Observable | observablehq.com | Yes | No |
| Desmos | desmos.com | Yes | No |
| Canva | canva.com | Yes | No |
Each service has default dimensions appropriate for its content type. YouTube embeds default to 800×450 (16:9), while Spotify defaults to 720×500. Vimeo starts at 640×360 and corrects itself to the video's real aspect ratio after creation (sizeToContentAspectRatio). Google Maps takes an API key through EmbedShapeUtil.configure({ embedConfig: { google_maps: { apiKey } } }).
Embed shapes behave differently from other shapes because they contain live interactive content. Clicking an embed selects the shape rather than interacting with the content. Double-click it (or press Enter while it's selected) to enter editing mode. In editing mode, pointer events pass through to the iframe so you can scroll, click buttons, and use the embedded application. Click outside the shape or press Escape to exit.
Locking an embed doesn't block this: ShapeUtil#canEditWhileLocked returns true for embeds (unless the definition sets canEditWhileLocked: false), so a locked embed can still enter editing mode and stays put while you use it. Editing also works in readonly mode.
The embed system converts user-facing URLs to embed URLs automatically. When you paste https://www.youtube.com/watch?v=dQw4w9WgXcQ, the embed shape stores the original URL and renders https://www.youtube.com/embed/dQw4w9WgXcQ.
Each embed definition includes two transformation functions:
toEmbedUrl: Converts a shareable URL to an embeddable URLfromEmbedUrl: Converts an embed URL back to the original URLThe shape stores the original URL and renders the embed version.
When you paste a URL that no embed definition recognizes, the default URL handler creates a bookmark shape instead of an embed. An existing embed shape whose URL doesn't match any definition still renders the URL in an iframe, using the stricter sandbox for unknown sources.
Use getEmbedInfo to check whether a URL is embeddable before creating a shape:
import { getEmbedInfo, DEFAULT_EMBED_DEFINITIONS } from 'tldraw'
const embedInfo = getEmbedInfo(DEFAULT_EMBED_DEFINITIONS, 'https://youtube.com/watch?v=abc123')
if (embedInfo) {
// URL is embeddable
console.log(embedInfo.definition.title) // "YouTube"
console.log(embedInfo.embedUrl) // "https://www.youtube.com/embed/abc123"
} else {
// URL is not embeddable, will render as bookmark
}
Embeds run in sandboxed iframes with restricted permissions. The defaults live in embedShapePermissionDefaults:
| Permission | Default | Description |
|---|---|---|
allow-scripts | Yes | Allow JavaScript execution |
allow-same-origin | Yes | Allow access to same-origin storage and APIs |
allow-forms | Yes | Allow form submission |
allow-popups | Yes | Allow opening new windows (for linking to source) |
allow-popups-to-escape-sandbox | No | Popups inherit the sandbox |
allow-downloads | No | Block file downloads |
allow-downloads-without-user-activation | No | Block automatic downloads |
allow-modals | No | Block modal dialogs like window.prompt() |
allow-orientation-lock | No | Block screen orientation lock |
allow-pointer-lock | No | Block pointer lock API |
allow-presentation | No | Block the Presentation API |
allow-top-navigation | No | Block navigating away from tldraw |
allow-top-navigation-by-user-activation | No | Block navigating away, even on user gesture |
allow-storage-access-by-user-activation | No | Block access to parent storage |
Individual embed definitions override these defaults with overridePermissions. YouTube and Google Maps allow allow-presentation for fullscreen; YouTube, Google Calendar, and Google Slides allow allow-popups-to-escape-sandbox; tldraw embeds allow allow-top-navigation so users can open rooms in new tabs.
GitHub Gist embeds receive special handling: they use srcDoc instead of src to load the gist script, drop allow-same-origin, and restrict gist IDs to hexadecimal characters. This prevents JSONP callback attacks. See the embed permissions example.
Replace or extend the default embed definitions using EmbedShapeUtil.configure(). Type custom definitions as CustomEmbedDefinition and give them an icon so they show up in the Insert embed dialog:
import { Tldraw, EmbedShapeUtil, DEFAULT_EMBED_DEFINITIONS, CustomEmbedDefinition } from 'tldraw'
import 'tldraw/tldraw.css'
const myService: CustomEmbedDefinition = {
type: 'myservice',
title: 'My Service',
hostnames: ['myservice.com'],
width: 600,
height: 400,
doesResize: true,
icon: 'https://myservice.com/favicon.png',
toEmbedUrl: (url) => {
const match = url.match(/myservice\.com\/item\/(\w+)/)
if (match) {
return `https://myservice.com/embed/${match[1]}`
}
return undefined
},
fromEmbedUrl: (url) => {
const match = url.match(/myservice\.com\/embed\/(\w+)/)
if (match) {
return `https://myservice.com/item/${match[1]}`
}
return undefined
},
embedOnPaste: true,
}
const shapeUtils = [
EmbedShapeUtil.configure({ embedDefinitions: [...DEFAULT_EMBED_DEFINITIONS, myService] }),
]
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw shapeUtils={shapeUtils} />
</div>
)
}
| Property | Type | Required | Description |
|---|---|---|---|
type | string | Yes | Unique identifier for this embed type |
title | string | Yes | Display name shown in the UI |
hostnames | string[] | Yes | URL hostnames to match (supports glob patterns like *.youtube.com) |
width | number | Yes | Default width in pixels |
height | number | Yes | Default height in pixels |
doesResize | boolean | Yes | Whether the shape can be resized |
toEmbedUrl | (url: string) => string | undefined | Yes | Convert shareable URL to embed URL |
fromEmbedUrl | (url: string) => string | undefined | Yes | Convert embed URL to shareable URL |
minWidth | number | No | Minimum width when resizing |
minHeight | number | No | Minimum height when resizing |
isAspectRatioLocked | boolean | No | Lock aspect ratio when resizing |
canEditWhileLocked | boolean | No | Allow interaction when shape is locked (default: true) |
overridePermissions | TLEmbedShapePermissions | No | Custom iframe sandbox permissions |
sizeToContentAspectRatio | boolean | No | Correct the size to the content's real aspect ratio after creation |
icon | string | No | Icon URL for the Insert embed dialog (CustomEmbedDefinition only) |
backgroundColor | string | No | Background color for the embed container |
overrideOutlineRadius | number | No | Custom border radius (Spotify uses 12px) |
embedOnPaste | boolean | No | When true, URLs are auto-converted to embeds on paste |
instructionLink | string | No | Help URL for services requiring setup (like Google Calendar public links) |
By default, pasting a URL from a supported service creates an embed shape. Set embedOnPaste: false in the embed definition to create a bookmark instead.
The tldraw embed definition sets embedOnPaste: false, so pasting a tldraw.com URL creates a bookmark. Separately, embed shapes refuse to render a nested tldraw canvas when the page itself is running inside an iframe, which prevents infinite nesting.
| Property | Type | Description |
|---|---|---|
url | string | The original URL (converted to embed URL internally); default '' |
w | number | Width of the embed container; default 300 |
h | number | Height of the embed container; default 300 |
Embed shapes render as blank rectangles in SVG exports. The iframe content can't be captured directly, so exports show a placeholder with the embed's background color and border radius.