Skip to content

Interactive Cells

Interactive cells are ordinary MDX code blocks with metadata comments at the top. Oxiquill extracts those blocks during generation and replaces them with a runtime UI.

Rust cells are compiled to WebAssembly at build time. Python cells run in a browser Pyodide worker. Haskell cells are compiled to a WASI WebAssembly module at build time. All three languages use the same input metadata and run-mode model.

Reruns retain the last successful output, with an error and retry action if execution fails. Cancellation and invalid input also keep the output. Compatible charts update the same canvas with a default 180ms transition, respecting OS reduced-motion preferences. See rich output for the exact style schema, limits, and Rust overloads. Explicit artifact IDs are recommended for dynamically changing output sequences.

Interactive fences use rust, python, or haskell (including Markdown’s {.rust} form) and may use legal backtick or tilde fences and indentation. Metadata is one contiguous block of language-appropriate option comments at the beginning of the fence: //| or ///| for Rust, #| for Python, and --| for Haskell. An option-looking comment after the first source line remains source code.

Only these top-level fields are accepted; unknown fields are errors:

FieldType and ruleDefault
idRequired lowercase kebab-case matching ^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$; unique within the page and after route scoping.
titleNon-empty string shown in the cell header; explicit values are trimmed.id
runbutton, reactive, or autorun.button
inputsMapping from lowercase cross-language identifiers matching ^[a-z][a-z0-9_]*$ to input specs. Not allowed with autorun.{}
packagesUnique non-empty Pyodide package names; Python only.[]
cratesUnique non-empty helper package names from direct children of cratesDir; Rust only.[]
timeoutMsInteger milliseconds from 1 through 2147483647, inclusive.30000
showSourceBoolean controlling initial source visibility.true

The source must contain non-metadata code. Malformed YAML, a wrong scalar/collection type, duplicate normalized ID/binding/function, unknown package/crate, or language-mismatched dependency field fails before output is written. Diagnostics include page path, fence start line, cell ID when available, and the exact field path.

Haskell cells use standard-library code only and accept neither packages nor crates. Rust cells without helpers should explicitly use crates: [] in examples. Package and crate lists are deduplicated and sorted after strict validation.

  • button: show inputs and a Run button; do not execute on mount.
  • reactive: show inputs, hide the Run button, execute once on mount, then use a 150 ms trailing debounce. At most one execution and one newest replacement are retained.
  • autorun: show neither inputs nor Run button and execute exactly once for each cell.id + runtimeVersion + source. Source updates during development invalidate the previous execution even if the runtime-version update arrives first.

This button cell waits for an explicit run even when its input changes.

Rust + Wasm

Explicit button execution

Cell actions
Cell inputs

Choose an integer from 1 through 10.

println!("button value = {value}");

Run the cell to show its output.

This autorun cell has no execution controls and runs once for the current generated runtime.

Rust + Wasm

One-time autorun execution

Cell actions
println!("autorun ready");

Waiting for the runtime to start.

Superseding a reactive run actively cancels its worker request, rejects every request affected by the worker recycle, clears their timers, and starts the latest complete values after the debounce. Cancellation is not shown as an execution error, and stale results cannot replace newer state. Timeout and worker-fault recovery use the same deterministic recycle boundary. Unmounting cancels pending and active work.

Every input accepts only fields relevant to its type. Common fields are type, label, description, and value; numeric inputs also use min, max, step, and integer, while select and radio use options.

TypeValue and constraints
range, numberFinite number; step > 0, min <= max, and the default must be within bounds and on the step grid.
integerSigned 32-bit integer from -2147483648 through 2147483647; bounds and step use the same domain.
text, textareaString value.
checkboxBoolean value.
select, radioNon-empty unique string options or { label, value } objects; the default must equal an option value.

type defaults to text, and label defaults to the input name. description is an optional non-empty string associated with the control for assistive technology. Default values are false for checkbox, 0 for numeric inputs, and an empty string for text inputs. Numeric-only fields are rejected on non-numeric controls, and options is rejected outside select and radio.

The signed 32-bit integer domain is exactly representable by JavaScript and maps consistently to Rust i32, Haskell Int, and Python int. Authoring rejects an integer value, min, max, or step outside that domain. Browser controls apply the same domain before constructing a worker request, including for integer: true numeric inputs.

Numeric defaults must align with the same step grid enforced by browser controls. The effective step is step when provided and 1 otherwise. The grid starts at min when provided; without min, the declared or default numeric value is the base. Oxiquill rejects a misaligned default during authoring instead of rounding or clamping it.

Number and integer controls keep their raw edit text separate from committed runtime values. Empty, incomplete, non-finite, out-of-range, and step-mismatched text remains visible but cannot update cell inputs. The Run button is disabled while any control is invalid, and reactive execution resumes exactly once with the latest complete value set after validity returns. Range labels derive their displayed precision from step.

The visible label is the control’s accessible name; the input key is only the generated language binding. Normalization must not make two Rust or Haskell bindings collide. Labels, descriptions, current values, validation messages, and grouped controls use stable IDs, keyboard focus remains visible, and run/error status is announced without stealing focus.

This Rust cell uses a helper crate and redraws a plot whenever the sliders change.

Rust + Wasm

Calculate the logistic map with Rust

Cell actions
Cell inputs
3.20
0.20
let steps = u32::try_from(steps).map_err(|_| "steps must be non-negative".to_owned())?;
let points = doc_rust::logistic_series(r, x0, steps).map_err(|error| error.to_string())?;

for point in points.iter().take(5) {
    println!("n={} x={:.6}", point.n, point.x);
}

emit_line_plot!(&points, "n", "x");

Waiting for the runtime to start.

A cell with crates: [] does not depend on helper crates. This example shows checkbox, select, and radio inputs.

Rust + Wasm

Rust input UI

Cell actions
Cell inputs
style
let base_score = match operation.as_str() {
    "triple" => 21_i32,
    "double" => 14_i32,
    _ => 7_i32,
};
let score = if include_bonus {
    base_score + 5
} else {
    base_score
};

println!("style = {style}");
println!("score = {score}");

Waiting for the runtime to start.

Each Rust cell declares the helper crates it needs. The names must match Cargo package names under crates/*.

Rust + Wasm

Rust cell with multiple crates

Cell actions
Cell inputs
style
let points = doc_rust::logistic_series(3.2, 0.2, 8).map_err(|error| error.to_string())?;
let final_point = points.last().ok_or_else(|| "series is empty".to_owned())?;

println!("{}", doc_rust_text::labeled_value("final step", final_point.n));
println!("{}", doc_rust_text::labeled_value("final x", format!("{:.6}", final_point.x)));
println!("{}", doc_rust_text::style_note(style.as_str()));

Waiting for the runtime to start.

Python cells use the same input metadata. Input values are available as Python variables.

Python + Pyodide

Python input UI

Cell actions
Cell inputs
style
values = [1, 2, 3, 4]

if method == "sum":
    result = sum(values) * scale
elif method == "max":
    result = max(values) * scale
else:
    result = sum(values) / len(values) * scale

name = label.upper() if enabled else label
print(f"{name}: {method} = {result}")
print(f"style = {style}")

Waiting for the runtime to start.

Haskell cells use --| metadata comments. Input values become Haskell variables, and text output written with putStrLn or print appears in the result panel.

Haskell + WASI

Haskell input UI

Cell actions
Cell inputs
{- Imports may use normal multiline Haskell layout. -}
import Data.List
  ( intercalate
  , sort
  )

let base = [1 .. 4 :: Int]
let scaled = map (* factor) base
let displayed = sort (if include_squares then map (\value -> value * value) scaled else scaled)

putStrLn (label ++ ": " ++ intercalate ", " (map show displayed))
putStrLn ("total = " ++ show (sum displayed))

Waiting for the runtime to start.

Python cells show Preparing Python… during initialization and package loading, then Running while authored code executes. See Python Runtime Assets for optional page preparation with python.preload.