Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 69 additions & 2 deletions docs/guides/publishing/embedding.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Embedding

There are various ways to embed marimo notebooks in other web pages, such
There are various ways to embed marimo notebooks in other web pages, such
as web documentation, educational platforms, or static sites in general. Here
are two ways:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

classic off-by-one error

are the main approaches:

* Host on [GitHub Pages](github_pages.md) or [self-host WASM HTML](self_host_wasm.md),
and `<iframe>` the published notebook.
Expand All @@ -12,3 +12,70 @@ are two ways:

We plan to provide more turn-key solutions for static site generation with
marimo notebooks in the future.

## Iframe Sandbox Configuration

When embedding marimo notebooks in sandboxed iframes, proper configuration is essential for full functionality. marimo is designed to gracefully degrade when certain features are restricted, but understanding these requirements will help you provide the best experience.

### Required Sandbox Attributes

For marimo to function properly in an iframe, you need this **minimum** sandbox attribute:

```html
<iframe
src="https://marimo.app/your-notebook"
sandbox="allow-scripts"
width="100%"
height="600"
></iframe>
```

* **`allow-scripts`**: Required for JavaScript execution (essential for marimo to run)

!!! note "Basic Functionality"
With only `allow-scripts`, marimo will work but with limitations: WebSocket connections will function, but storage will be in-memory only (state resets on page reload), and clipboard access will use browser prompts instead of the clipboard API.

### Recommended Sandbox Attributes

For the best user experience, include these additional attributes:

```html
<iframe
src="https://marimo.app/your-notebook"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups"
allow="microphone"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

webcam too?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not needed for our components

allowfullscreen
width="100%"
height="600"
></iframe>
```

**Additional Attributes:**

* **`allow-same-origin`**: Enables persistent storage (localStorage) and full clipboard API. Only use this if you trust the content of the iframe or the iframe URL is hosted on a different domain.
* **`allow-downloads`**: Enables downloading notebook outputs, data exports, and screenshots
* **`allow-popups`**: Allows opening links and notebooks in new tabs
* **`allowfullscreen`** (attribute, not sandbox): Enables fullscreen mode for slides and outputs

**Permission Policy:**

* **`allow="microphone"`**: Required for `mo.ui.microphone()` widget functionality
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh ok. We could add mo.ui.webcam at some point. But probably with demand


!!! tip "Security Considerations"
Only use `allow-same-origin` with trusted content or the iframe URL is hosted on a different domain. Combining `allow-scripts` and `allow-same-origin` allows the iframe to remove the sandbox attribute entirely, making the iframe as powerful as if it weren't sandboxed at all.

### Example: Full Configuration

Here's a complete example with all recommended settings:

```html
<iframe
src="https://marimo.app/l/your-notebook-id?embed=true&mode=read"
sandbox="allow-scripts allow-same-origin allow-downloads allow-popups allow-downloads-without-user-activation"
allow="microphone"
allowfullscreen
width="100%"
height="600"
style="border: 1px solid #ddd; border-radius: 8px;"
></iframe>
```
10 changes: 6 additions & 4 deletions docs/guides/publishing/playground.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ providing your users with an interactive code playground.
width="100%"
height="500"
frameborder="0"
sandbox="allow-scripts"
></iframe>
```

Expand All @@ -201,7 +202,6 @@ providing your users with an interactive code playground.
To show editor controls (such as panels icons and the run button), use
the query parameter `show-chrome=true`


### Embedding an existing notebook

To embed existing marimo notebooks into a webpage, first, [obtain a
Expand All @@ -213,6 +213,7 @@ URL to your notebook](#creating-and-sharing-playground-notebooks), then put it i
width="100%"
height="500"
frameborder="0"
sandbox="allow-scripts"
></iframe>
```

Expand All @@ -229,6 +230,7 @@ You can optionally render embedded notebooks in read-only mode by appending
width="100%"
height="500"
frameborder="0"
sandbox="allow-scripts"
></iframe>
```

Expand Down Expand Up @@ -269,7 +271,7 @@ For example, if you are using MDX, you can use the following snippet:
```jsx
const MdxNotebook = (props: { code: string }) => {
return (
<iframe src={`https://marimo.app?embed=true&show-chrome=false&code=${encodeURIComponent(props.code)}`} />
<iframe src={`https://marimo.app?embed=true&show-chrome=false&code=${encodeURIComponent(props.code)}`} sandbox="allow-scripts" />
);
};

Expand All @@ -287,6 +289,6 @@ def _():
@app.cell(hide_code=True)
def _():
...
return
`} />
return
`} />
```
32 changes: 18 additions & 14 deletions frontend/src/components/editor/Output.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
ExpandIcon,
} from "lucide-react";
import { useExpandedOutput } from "@/core/cells/outputs";
import { useIframeCapabilities } from "@/hooks/useIframeCapabilities";
import { renderHTML } from "@/plugins/core/RenderHTML";
import { Banner } from "@/plugins/impl/common/error-banner";
import type { TopLevelFacetedUnitSpec } from "@/plugins/impl/data-explorer/queries/types";
Expand Down Expand Up @@ -378,6 +379,7 @@ const ExpandableOutput = React.memo(
const containerRef = useRef<HTMLDivElement>(null);
const [isExpanded, setIsExpanded] = useExpandedOutput(cellId);
const [isOverflowing, setIsOverflowing] = useState(false);
const { hasFullscreen } = useIframeCapabilities();

// Create resize observer to detect overflow
useEffect(() => {
Expand All @@ -403,20 +405,22 @@ const ExpandableOutput = React.memo(
<div>
<div className="relative print:hidden">
<div className="absolute -right-9 top-1 z-1 flex flex-col gap-1">
<Tooltip content="Fullscreen" side="left">
<Button
data-testid="fullscreen-output-button"
className="hover-action hover:bg-muted p-1 hover:border-border border border-transparent"
onClick={async () => {
await containerRef.current?.requestFullscreen();
}}
onMouseDown={Events.preventFocus}
size="xs"
variant="text"
>
<ExpandIcon className="size-4" strokeWidth={1.25} />
</Button>
</Tooltip>
{hasFullscreen && (
<Tooltip content="Fullscreen" side="left">
<Button
data-testid="fullscreen-output-button"
className="hover-action hover:bg-muted p-1 hover:border-border border border-transparent"
onClick={async () => {
await containerRef.current?.requestFullscreen();
}}
onMouseDown={Events.preventFocus}
size="xs"
variant="text"
>
<ExpandIcon className="size-4" strokeWidth={1.25} />
</Button>
</Tooltip>
)}
{(isOverflowing || isExpanded) && !forceExpand && (
<Button
data-testid="expand-output-button"
Expand Down
46 changes: 25 additions & 21 deletions frontend/src/components/slides/slides-component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { Swiper, type SwiperRef, SwiperSlide } from "swiper/react";
import { Button } from "@/components/ui/button";
import { useEventListener } from "@/hooks/useEventListener";
import { useIframeCapabilities } from "@/hooks/useIframeCapabilities";
import { cn } from "@/utils/cn";

import "./slides.css";
Expand All @@ -30,6 +31,7 @@ const SlidesComponent = ({
}: PropsWithChildren<SlidesComponentProps>): JSX.Element => {
const el = React.useRef<SwiperRef>(null);
const [isFullscreen, setIsFullscreen] = React.useState(false);
const { hasFullscreen } = useIframeCapabilities();

useEventListener(document, "fullscreenchange", () => {
if (document.fullscreenElement) {
Expand Down Expand Up @@ -103,28 +105,30 @@ const SlidesComponent = ({
</SwiperSlide>
);
})}
<Button
variant="link"
size="sm"
data-testid="marimo-plugin-slides-fullscreen"
onClick={async () => {
if (!el.current) {
return;
}
const domEl = el.current as unknown as HTMLElement;
{hasFullscreen && (
<Button
variant="link"
size="sm"
data-testid="marimo-plugin-slides-fullscreen"
onClick={async () => {
if (!el.current) {
return;
}
const domEl = el.current as unknown as HTMLElement;

if (document.fullscreenElement) {
await document.exitFullscreen();
setIsFullscreen(false);
} else {
await domEl.requestFullscreen();
setIsFullscreen(true);
}
}}
className="absolute bottom-0 right-0 z-10 mx-1 mb-0"
>
{isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
</Button>
if (document.fullscreenElement) {
await document.exitFullscreen();
setIsFullscreen(false);
} else {
await domEl.requestFullscreen();
setIsFullscreen(true);
}
}}
className="absolute bottom-0 right-0 z-10 mx-1 mb-0"
>
{isFullscreen ? "Exit Fullscreen" : "Fullscreen"}
</Button>
)}
</Swiper>
);
};
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/hooks/useIframeCapabilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { useMemo } from "react";
import {
getIframeCapabilities,
type IframeCapabilities,
} from "@/utils/capabilities";

/**
* React hook to access iframe capabilities
*/
export function useIframeCapabilities(): IframeCapabilities {
return useMemo(() => getIframeCapabilities(), []);
}
1 change: 1 addition & 0 deletions frontend/src/plugins/core/sanitize.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* Copyright 2024 Marimo. All rights reserved. */
import DOMPurify, { type Config } from "dompurify";
import { atom, useAtomValue } from "jotai";
import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
Expand Down
Loading
Loading