@notionhq/custom-blocks
v0.1.8
Published
> [!NOTE] > **Pre-release.** Custom blocks are in private alpha. Breaking changes may land at any time.
Downloads
5,646
Maintainers
Keywords
Readme
@notionhq/custom-blocks
[!NOTE] Pre-release. Custom blocks are in private alpha. Breaking changes may land at any time.
SDK for building Notion custom blocks.
A custom block runs as a sandboxed <iframe> inside the Notion app that has no direct access to the internet. The only channel between your block and Notion is a local postMessage bridge. This library implements the sandbox side of the bridge protocol and wraps it in a framework-neutral TypeScript API (@notionhq/custom-blocks) and typed React hooks (@notionhq/custom-blocks/react).
Install
Create a worker project with ntn workers new --template custom and use the dependencies and
entrypoint it generates. In this repository, fixtures depend on the SDK through
the workspace. React is optional unless the block imports
@notionhq/custom-blocks/react. React blocks need react and react-dom.
For local preview, run ntn customblocks dev from the worker project, then
open http://localhost:9873.
Quick start
Declare your custom block along with the rest of your worker definition:
// src/index.ts
import { Worker } from "@notionhq/workers";
const worker = new Worker();
export default worker;
worker.customBlock("hello", {
path: "./blocks/hello",
command: "npx vite build",
});In the block's entry point defined above, wrap your React code in <NotionCustomBlock> so the SDK connects with Notion before anything renders:
// blocks/hello/src/index.tsx
import "@notionhq/custom-blocks/nds.css";
import {
NotionCustomBlock,
NotionTokenScope,
} from "@notionhq/custom-blocks/react";
import ReactDOM from "react-dom/client";
import { App } from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<NotionCustomBlock>
<NotionTokenScope>
<App />
</NotionTokenScope>
</NotionCustomBlock>,
);Hooks can then read live content directly from Notion:
// blocks/hello/src/App.tsx
import { useBlockId } from "@notionhq/custom-blocks/react";
export function App() {
const blockId = useBlockId();
return <div className="app">Hello from {blockId}.</div>;
}.app {
color: var(--content-primary);
background: var(--bg-base);
padding: var(--spacing-12);
border: 1px solid var(--border-primary);
border-radius: var(--radius-12);
}<NotionCustomBlock> runs the SDK ↔ host handshake (connect → init → initResult) and only mounts children once it resolves. Inside the wrapper, every hook returns non-nullable values — there's no separate gating component to write. It also runs useCustomBlockAutoResize for you by default; pass autoResize={false} to opt out.
Notion design tokens
The optional @notionhq/custom-blocks/nds.css stylesheet provides the colors, spacing, typography, borders, and other design tokens used by Notion. Import it once, then put token-consuming UI inside <NotionTokenScope> as shown above.
<NotionTokenScope> applies the display and contrast modes selected by the host, so variables such as --content-primary and --bg-base stay in sync with Notion automatically. Hosts that do not provide a contrast mode use standard contrast.
NotionTokenScopeProps has one required children prop.
The stylesheet is scoped rather than installed globally. Render portals inside <NotionTokenScope>, or wrap the portal container in another scope. Framework-neutral renderers can read the current appearance with customBlock.getTheme() and customBlock.getContrastMode(); see Block Location & Appearance.
Reference
API surface, one page per category. Import framework-neutral APIs from @notionhq/custom-blocks; import React hooks and components from @notionhq/custom-blocks/react. Hover docs in your editor cover the per-field detail; these pages cover usage shape and the gotchas.
docs/lifecycle.md—<NotionCustomBlock>,useCustomBlockInit,initCustomBlock,customBlock.autoResize,NotInIframeError,useCustomBlockAutoResize. The handshake, the React wrapper, sizing.docs/block-location.md—useBlockId,useParent,usePage,useTheme, anduseContrastMode. Where the block sits and how to read the host's appearance.docs/data-sources.md—useDataSource,useManifest,customBlock.getManifest, the row, property, and date-value types, plus a worked example.docs/pages.md—pages.create / get / update / delete, parent variants (including the recommendeddata_source_key), property input shapes.docs/users.md—users.list / get, theNotionUsershape, paging.docs/errors.md— request results, error format, error codes, retries, and initialization failures.docs/deployment.md— worker-backed deploys, localhost self-hosted fallback, where the manifest comes from.docs/vite-plugin.md— thenotionCustomBlock()Vite plugin.
Forbidden APIs
No top-level navigation, window.open, or auth redirects. No direct network requests — cross-origin work goes through the host (and is exposed via SDK hooks).
Bridge protocol
The bridge speaks a versioned postMessage protocol, but block authors should
use this package's public APIs rather than send bridge messages directly.
@notionhq/custom-blocks-protocol and @notionhq/custom-blocks-host are
internal workspace packages and are not available to block authors.
