snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View File

@@ -0,0 +1,226 @@
---
title: Embed shape
created_at: 1/31/2026
updated_at: 1/31/2026
keywords:
- embed
- youtube
- figma
- iframe
- codesandbox
- codepen
- google maps
- spotify
- vimeo
status: published
date: 1/31/2026
---
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](?).
## Creating embeds
Paste a supported URL onto the canvas, or create an embed shape programmatically:
```tsx
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.
### Pasting iframe code
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.
## Supported services
| 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 } } })`.
## Interacting with embeds
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.
## URL transformation
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 URL
- `fromEmbedUrl`: Converts an embed URL back to the original URL
The shape stores the original URL and renders the embed version.
## Fallback to bookmarks
When you paste a URL that no embed definition recognizes, the default URL handler creates a [bookmark shape](/sdk-features/default-shapes#Bookmark) 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:
```tsx
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
}
```
## Iframe security
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](/examples/configuration/embed-permissions).
## Custom embed definitions
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:
```tsx
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>
)
}
```
### Embed definition properties
| 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) |
## Embed-on-paste behavior
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. Pasting while a single shape that can hold a link is selected sets that shape's link rather than creating either.
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.
## Shape properties
| 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` |
## SVG export
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.
## Related articles
- [Default shapes](/sdk-features/default-shapes) — Overview of all built-in shape types
- [Bookmark shape](/sdk-features/default-shapes#Bookmark) — URL link cards (fallback for non-embeddable URLs)
- [External content handling](/sdk-features/external-content) — Handling pasted and dropped content
- [Custom embeds example](/examples/configuration/custom-embed) — Add a custom embed definition for a new service