I've packaged this article up and released a library, @celine/celine. It has a nicer API too. Check it out! (o˘◡˘o)
Published
Modified
Hacker News discussion
SydJS talk

Reactive HTML notebooks

Before I start, why am I doing this?

I don't think HTML is being used enough as a platform for scientific publishing.

Instead, people will:
  1. Use an interactive notebook like Jupyter, RStudio, Pluto.jl or Observable to do data exploration, analysis and visualisation,
  2. Move to a publishing platform like Typst, Overleaf, pure LaTeX, or a WYSIWYG editor to typeset their work,
  3. Export to .pdf for distribution.

I think a HTML file can be used for all 3 of these stages, and prevent a lot of faffing around with manual processes, CLI tooling, CI steps and 3rd-party platforms.

HTML's typesetting capabilities are well documented, but its capabilities as a platform for data exploration, analysis and visualisation are not.

I'll try and demonstrate these capabilities, literate programming style.
A computer displays an open book

Cells

First, we'll steal a trick from This page is a truly naked, brutalist html quine, and create a CSS class called echo that will display/reflect style and script elements inline.

Add in a font with built-in syntax highlighting and a contenteditable attribute and we have a basic code editor!

I stress that this style element is styling itself to be visible.

Try changing .echo's background-color!
We also want contenteditable scripts to be re-evaluated on blur by building a clone of the script and then removing the original.
(Why can't we just use eval? For one, eval doesn't work with code with import statements in it.)
Now we'll import the Observable standard library and the Observable runtime, and bind them to window. We'll export only 2 symbols to window, library and cell.
Now we'll declare a cell called counter that emits a number every second. The script's id attribute is the same as the name parameter passed to cell.

Try changing the initial counter value i above to a much bigger number, and then defocus the script.
Now that we've created a our counter cell, we can create other cells that depend on it.
We'll import Hypertext Literal and use it to format the counter value. htl implements a full-blown HTML5 parser that performs automatic escaping and interpolation of non-serializable values, such as event listeners, style objects, and other DOM nodes.
We can still observe the output of a cell without needing to show its definition. Just don't add the echo class. This makes them useful as a rendering primitive. (There's a hidden cell above ^)


Alternatively, we can create a cell type that doesn't display its output at all.
We can use these cells to store intermediate values or datastructures. Also note that cells can be declared out of order.

We can use cell values in more complex outputs. We'll import Observable Plot and use the counter value in a plot.
A computer displays a graph in the upward direction

TeX, Markdown, Graphviz

We can return any type of DOM element from a cell.
In this case, the tex, md, and dot cells return span, table and svg elements respectively.

Try editing any of the following cells.
A computer with an open CD tray is surrounded by data

Cell status

We can also return a Promise, or throw an Error, from a cell. Observable's Inspector will apply an observablehq--running or observablehq--error class to the cell's outer div element respectively. We'll style them appropriately:

SQLite

I've hosted the Chinook sample database on my website at https://maxbo.me/chinook.db. Now we'll use a WASM-backed SQLite client to query it.

Try adding WHERE Milliseconds < 1000000 to the SQL query!

Python

The Pyodide CPython WASM distribution includes NumPy, Pandas, Matplotlib, scikit-learn, and Scipy. We'll rebuild the plot seen above, but using Matplotlib and Python's sqlite3 module instead.

Try editing one of the plot labels!

R

You know the drill. It's R, using WebR. I didn't figure out how to get ggplot2 rendering working, but I assume it's possible. I must disclose that this cell seems to be a bit flaky on iOS. I have not had a chance to investigate further, nor will I.

Inputs

We'll create a new cell type viewof that works specifically with Observable Inputs. It declares 2 reactive cells: NAME and viewof NAME - one for the value, and one for the DOM element itself.
To display the input above the cell, we set the cell id to viewof NAME.

Wiggle the range input and see another dependent cell update.
NB: The way Observable Inputs work is a bit arcane. This demo of Synchronized Inputs may shed some light.

Mutability

Purely functional dataflow is great, but sometimes you just need to mutate state. We'll create a new helper function mutable. It registers a Mutable - an object that yields new Generator values when the value is mutated - in the runtime.
Try editing the initial state of the mutable. Try editing the button labels.

What's next?

I will try and cram all of this into a library with some proper documentation.
I initially thought it should be called incel (short for inline cell), but I'll probably call it celine instead.

I've released a library! It's called @celine/celine!
A computer terminal receives text

Slide infrastructure

I demo'd this article at SydJS. This is the code I used to turn the article into a slideshow.