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.
A demo is a component that gets name and params. Make it a Client Component to use state or animation:
// 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:
// lib/demos.tsimport type { Demos } from "notionic"import { MorphButton } from "@/components/demos/morph-button"
export const demos: Demos = { "morph-button": MorphButton }<NotionRenderer blocks={blocks} demos={demos} />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=slowis{ speed: "slow" }. - Unknown names render as links, so a typo never breaks the page.
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:
// 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.
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:
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 }} />The page stays a Server Component. Only the demos a page renders send JavaScript to the browser, so a page without demos ships none.