Observable Notebook Kit
Observable Notebook Kit is an open-source command-line tool for building static sites from Observable Notebooks based on an open file format. Notebook Kit also includes a Vite plugin and a low-level JavaScript interface for deep integration of Observable Notebooks with custom web applications.
Notebook Kit is available as part of Notebooks 2.0 Technology Preview, which includes Observable Desktop, a macOS application for editing notebooks.
For more on authoring notebooks, see the Notebooks system guide.
Installing
You can install Notebook Kit from npm:
npm add @observablehq/notebook-kit
If you use pnpm, you must also explicitly install any fonts used by the default styles (unless you plan on using different fonts):
pnpm add @observablehq/notebook-kit @fontsource/inter @fontsource/source-serif-4 @fontsource/spline-sans-mono
If you plan on using database connectors, you must also install the drivers for the databases you wish to use. To install the DuckDB driver:
npm add @duckdb/node-api
To install the Postgres driver:
npm add postgres
To install the Snowflake driver:
npm add snowflake-sdk
To install the Google BigQuery driver:
npm add @google-cloud/bigquery
To install the Databricks driver:
npm add @databricks/sql
Notebook Kit requires Node.js 20.19+ or 22.12+. To use the SQLite database connector, you must use Node.js 24+. Bun 1.2+ should also work, but is not officially supported.
The notebook file format
The notebook file format is intended to be simple, human-readable, and human-editable. It’s based on HTML, which means you get nice editing affordances in today’s text editors without needing special plugins. In addition, it’s easy to review diffs when storing notebooks in source control, to search, to find-and-replace, and countless other workflows.
A notebook HTML file consists of:
- a
<notebook>
root element, - an optional
<title>
element, and - one
<script>
element per cell.
Here’s a simple “hello world” notebook to demonstrate:
notebook>
<title>Hello, world!</title>
<script type="text/markdown">
# Hello, world!
</script>
<script type="module" pinned>
1 + 2
</script>
</notebook>
<
The cell <script>
element should use four spaces of indentation for each line; these four leading spaces are trimmed when parsing the notebook. (In the future, we’ll likely make this contextual to allow varied indentation.)
The <script>
element supports the following attributes:
type
- the cell type (also known as the cell language or cell mode)pinned
- if present, show the cell’s source codehidden
- if present, suppress the cell’s implicit displayoutput
- for non-JavaScript cells, how to expose the valuedatabase
- for SQL cells, which database to queryid
- the cell identifier, for stable editing (optional)
The type
attribute can have one of the following values:
module
- JavaScripttext/markdown
- Markdowntext/html
- HTMLapplication/sql
- SQLapplication/x-tex
-text/vnd.graphviz
- DOT (Graphviz)application/vnd.observable.javascript
- Observable JavaScript
Here’s an example of each supported cell type.
A JavaScript (module
) cell:
<script type="module">
1 + 2
</script>
A Markdown (text/markdown
) cell:
<script type="text/markdown">
# Hello, world!
</script>
An HTML (text/html
) cell:
<script type="text/html">
<h1>Hello, <i>world</i>!</h1>
</script>
A SQL (application/sql
) cell:
<script type="application/sql" database="reporting" output="customers">
SELECT * FROM customers
</script>
A (application/x-tex
) cell:
<script type="application/x-tex">
\int_{-\infty}^{\infty} e^{-x^2} dx = \sqrt{\pi}
</script>
A DOT (text/vnd.graphviz
) cell:
<script type="text/vnd.graphviz">
digraph G {
A -> B -> C;
A -> C;
}
</script>
An Observable JavaScript (application/vnd.observable.javascript
) cell:
<script type="application/vnd.observable.javascript">
foo = 42
</script>
The pinned
attribute determines whether to show the cell’s source code. The hidden
attribute suppresses the cell’s implicit display; for example, you can use it hide a SQL cell’s results table. Cells can also have an id
attribute to indicate a unique (positive integer) identifier; an id
isn’t required, but gives cells a stable identity for editing.
Since each cell is specified as a <script>
element, if the cell source code contains a </script>
, it must be escaped with a backslash as <\/script>
. Likewise, a sequence of backslashes in this context must be escaped with an additional backslash, such as <\\/script>
.
The <notebook>
element supports the following attributes:
theme
- the themereadonly
- if present, disallow editing
The theme
attribute may have one of the following values:
air
(default)coffee
cotton
deep-space
glacier
ink
midnight
near-midnight
ocean-floor
parchment
slate
stark
sun-faded
See Observable Framework for theme previews. More themes may be added in the future. Built-in themes can be augmented using standard stylesheets.
See notebook.ts
for TypeScript types representing notebooks and cells.
The page template format
Notebook Kit supports custom page templates when previewing and building notebooks. Page templates can provide additional chrome around the notebook, such as a header and footer. A page template HTML file can contain whatever you like. Notebook Kit will insert or modify the following elements:
<meta name="generator">
- Notebook Kit version and attribution<title>
- a title suffix, appearing after the notebook title<main>
- where to render the notebook cells
The default template looks something like this:
html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style type="text/css">
@import url("observable:styles/index.css");
</style>
</head>
<body>
<main></main>
</body>
</html>
<
Command-line interface
Notebook Kit’s CLI supports four commands: preview
for a live preview of notebooks, build
for building a static site, download
for downloading Observable Notebooks as HTML, and query
for saving the results of database queries.
We recommend that you install Notebook Kit locally to a project using a package manager such as npm, and then add preview and build scripts to your package.json
.
{
"dependencies": {
"@observablehq/notebook-kit": "^1.2.0"
},
"scripts": {
"docs:preview": "notebooks preview --root docs",
"docs:build": "notebooks build --root docs -- docs/*.html"
}
}
Preview
To start the preview server, run the preview
command. Use the --root
argument to tell the preview server where the notebooks are located.
notebooks preview --root docs
If you’d like to use a custom page template, use the --template
argument:
notebooks preview --root docs --template docs/custom.tmpl
Build
To convert notebooks into a static site, run the build
command. Use the --root
argument to tell the command where the notebooks are located; you must also provide a list of notebook HTML files as positional arguments. By default, the site will be built in .observablehq/dist
within the root.
notebooks build --root docs -- docs/*.html
Or, with a custom template:
notebooks build --root docs --template docs/custom.tmpl -- docs/*.html
Download
To download and convert an existing Observable Notebook to a local HTML file, run the download
command.
notebooks download https://observablehq.com/@d3/bar-chart > docs/bar-chart.html
Query
Run the query
command to save the result of a database query. (This is typically done automatically, but can be used to prepare or refresh queries manually.) Pass positional arguments to interpolate JSON-encoded values into the query.
notebooks query --database duckdb 'SELECT 1 + ' '2'
JavaScript interface
For deeper integration of Observable Notebooks into custom web applications, we provide a Vite plugin and a low-level JavaScript interface. For more, please browse the TypeScript source, or start a discussion if you’d like help.
Vite API
The Vite plugin is recommended for Vite-based applications.
observable({template?: string, transformTemplate?: (template: string) => string | Promise<string>, transformNotebook?: (notebook: Notebook) => Notebook | Promise<Notebook>, window?: Window, parser?: DOMParser} = {}): PluginOption
Returns a Vite plugin for rendering notebook HTML as vanilla HTML. Markdown and HTML cells are rendered statically (without any interpolated dynamic values), and then, if needed, replaced with dynamic content when the page loads. Other cell modes are exclusively dynamic. Pinned cells display their source code statically rendered below any output. Supported languages are syntax-highlighted using Lezer.
config(): UserConfig
Returns the base Vite config needed to use the observable()
Vite plugin; this enables top-level await, multi-page application behavior, and adds resolvers for npm:
, jsr:
, and observable:
import protocols.
Build API
The Build API analyzes cell source code to find unbound references, such that the appropriate variable graph can be initialized using the Observable Runtime. For non-JavaScript cells (such as Markdown, HTML, and SQL), the Build API also transpiles the source into a tagged template literal expression.
maybeParseJavaScript(input: string): JavaScriptCell | undefined
Parses the specified JavaScript cell source code, returning the resulting AST as either a Program
or Expression
node, along with any top-level variable declarations
and unbound references
to other variables. If the specified input
has invalid syntax, returns undefined
.
parseJavaScript(input: string): JavaScriptCell
Parses the specified JavaScript cell source code, returning the resulting AST as either a Program
or Expression
, along with any top-level variable declarations
and unbound references
to other variables. (This method is called internally by transpileJavaScript
.)
parseTemplate(input: string): TemplateLiteral
Parses the specified template cell source code (such as the contents of a Markdown or SQL cell), returning the resulting AST as a TemplateLiteral
. (This method is called internally by transpile
.)
transpile(input: Cell, options?: TranspileOptions): TranspiledJavaScript
Transpiles the specified cell. The transpiled source is returned as a body
function suitable for use with variable.define
from the Observable Runtime, along with any named inputs
(unbound references) and outputs
(top-level declarations).
transpileJavaScript(input: string, options?: TranspileOptions): TranspiledJavaScript
Transpiles the specified JavaScript cell source code. The transpiled source is returned as a body
function suitable for use with variable.define
from the Observable Runtime, along with any named inputs
(unbound references) and outputs
(top-level declarations).
transpileTemplate(input: Cell): string
Transpiles the specified template cell, such as a Markdown or SQL cell, returning the corresponding JavaScript source code consisting of a single template literal expression. (This method is called internally by transpile
, and the result is passed to transpileJavaScript
.)
Utility API
These utility methods define default values, allowing shorthand.
toNotebook(spec: NotebookSpec): Notebook
Given a NotebookSpec
, returns the corresponding Notebook
, populating any default values.
toCell(spec: CellSpec): Cell
Given a CellSpec
, returns the corresponding Cell
, populating any default values.
defaultPinned(mode: Cell["mode"]): boolean
Returns the default pinned
value for the given cell mode
.
serialize(notebook: Notebook, {document?: Document} = {}): string
Serializes the specified notebook to HTML.
deserialize(html: string, {parser?: DOMParser} = {}): Notebook
Deserializes the specified HTML to a notebook.
Runtime API
The Runtime API provides some additional logic on top of the Observable Runtime to run the notebook, allowing cells to be redefined at runtime, and implementing the notebook standard library, including the display
and view
functions which are scoped per-cell.
constructor NotebookRuntime(builtins?: Record<string, unknown>)
Instantiates a new notebook runtime, given the specified built-ins, which defaults to (the standard) library
. The returned instance exposes a runtime
, main
, and define
methods which are equivalent to those below, but scoped to this specific instance. This constructor allows multiple notebooks to be instantiated concurrently.
define(state: DefineState, definition: Definition, observe?: () => Observer): void
Defines a cell, given the specified state
(including the root
in which to display the cell’s output, and recording any secondary variables
defined by the cell) and definition
(including the cell’s id
, body
, inputs
, outputs
, etc.). An observe
function may be specified to override the default variable observer, which is used to render the cell’s contents.
display(state: DisplayState, value: unknown): void
Displays a cell’s value (into the root
given by the specified state
). (This method is used internally when you call display
from a cell.)
clear(state: DisplayState): void
Clears a cell’s output (from the root
given by the specified state
). (This method is used internally to clear the display when a cell is invalidated.)
observe(state: DisplayState, definition: Definition): void
Returns the default variable observer used to render the cell’s contents.
inspect(value: unknown, expanded?: number[][]): HTMLDivElement
Returns the rendered inspector for the given value
. This is a small wrapper on top of the Observable Inspector’s inspector.fulfilled
which re-expands collapsible elements when they are re-rendered.
inspectError(value: unknown): HTMLDivElement
Returns the rendered inspector for the given error value
. This is a small wrapper on top of the Observable Inspector’s inspector.rejected
.
getExpanded(node: Node): number[][] | undefined
Given an inspector node previously rendered by inspect(…)
, returns the list of paths that have been expanded by the user; passing these to a subsequent call to inspect(…)
will re-expand the same set of paths after re-rendering.
root: HTMLElement
Returns the root element into which cells are rendered. This is the first <main>
element, if it exists, and otherwise the <body>
.
runtime: Runtime
The Observable Runtime instance.
main: Module
The main runtime module.
library: Record<string, unknown>
The built-ins provided to the runtime; the notebook standard library. This object includes the definitions for various built-ins, such as now
, width
, Generators
, and Mutable
.
Database API
The Database API provides a low-level API for running database queries.
getDatabaseConfig(sourcePath: string, databaseName: string): Promise<DatabaseConfig>
Loads the database config given a path to a notebook source file and the name of a database.
getDatabase(config: DatabaseConfig): Promise<QueryTemplateFunction>
Returns the query template function (equivalent to the sql
tagged template literal) for running a query against the given database.
getQueryCachePath(sourcePath: string, databaseName: string, strings: readonly string[], …params: unknown[]): Promise<string>
Returns the path to the query results cache file, given a path to a notebook source file, the name of a database, and query template parts.