Skip to contentnotionic
Docs menu

Guides

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.

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.tsimport 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 (opens in a new tab), 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.tsximport { 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.