# Interactive demos

> Put your own React components in a Notion page.

Show interactive components in a page, like the demos in a design engineering blog. A demo is a component in your project, so it can use anything you install: Motion, Sonner, charts, 3D. In Notion, an embed says where it goes.

[Press on the button to see it morph.](https://notionic.dev/demos/morph-button)

## Write a demo

A demo is a component that gets `name` and `params`. Make it a Client Component to use state or animation:

```typescript
// components/demos/morph-button.tsx
"use client"

import { motion } from "motion/react"
import type { DemoProps } from "notionic"

export function MorphButton({ params }: DemoProps) {
  // …
}
```

List your demos by name, and pass them to the renderer:

```typescript
// lib/demos.ts
import type { Demos } from "notionic"
import { MorphButton } from "@/components/demos/morph-button"

export const demos: Demos = { "morph-button": MorphButton }
```

```typescript
<NotionRenderer blocks={blocks} demos={demos} />
```

## Place it in Notion

Type `/embed` and paste a URL that ends in `/demos/<name>`, such as `https://your-site.com/demos/morph-button`. The embed's caption shows below the demo.

- **Only the path counts**, so the same embed renders on [localhost](http://localhost/), previews and production.
- **Variations.** The URL's query reaches the demo as `params`: `?speed=slow` is `{ speed: "slow" }`.
- **Unknown names** render as links, so a typo never breaks the page.

## Preview it in Notion

Notion shows an embed's page inside the editor. Add a page at `/demos/[name]` that renders the demo alone, and you see the demo live while you write:

```typescript
// app/demos/[name]/page.tsx
import { notFound } from "next/navigation"
import { demos } from "@/lib/demos"

export default async function DemoPage({ params }: { params: Promise<{ name: string }> }) {
  const { name } = await params
  const Demo = Object.hasOwn(demos, name) ? demos[name] : undefined
  if (!Demo) notFound()
  return (
    <main className="grid min-h-svh place-items-center p-6">
      <Demo name={name} params={{}} />
    </main>
  )
}
```

Notion loads it from your deployed site, so a new demo shows up in the editor once it's deployed. Your local site renders it right away.

## Change the frame

Demos render in `DemoFrame`: a bordered stage with the caption centered below. Replace it with `components.demo`, which gets the demo as `children`, plus `name`, `caption` and `context`:

```typescript
import { RichText, type DemoFrameProps } from "notionic"

function Frame({ caption, context, children }: DemoFrameProps) {
  return (
    <figure>
      <div className="grid min-h-64 place-items-center rounded-2xl bg-muted">{children}</div>
      <figcaption className="mt-3 text-center text-sm text-muted-foreground">
        <RichText text={caption} context={context} />
      </figcaption>
    </figure>
  )
}

<NotionRenderer blocks={blocks} demos={demos} components={{ demo: Frame }} />
```

## What it costs

The page stays a Server Component. Only the demos a page renders send JavaScript to the browser, so a page without demos ships none.
