Skip to content

The renderer contract

A slide is HTML, so nothing about it requires React. An island is one piece of another framework living inside a slide, and a renderer is everything the engine knows about that framework: how to put a component into an element, and how to take it out again.

// src/main.tsx — once, where the deck is created
import { registerLazyRenderer } from "@kadal/deck";
registerLazyRenderer("vue", () => import("@kadal/deck-vue").then((m) => m.vueRenderer));
<Island
framework="vue"
load={() => import("./chart.ts").then((m) => m.Chart)}
props={{ data }}
client="slide"
/>
Prop Meaning
component the component itself, in whatever framework wrote it
load a dynamic import of it instead — the component and (with registerLazyRenderer) its framework stay out of the deck’s first load
framework name the renderer. Otherwise the build’s stamp decides, then owns()
props passed to the component; compared shallowly, so new object identities alone do not remount
client when it comes alive (below)
fallback shown before the island is alive, and instead of it where it never will be (print, previews)
className, children a class on the host; children rendered to HTML and handed over as a slot

client strategies: slide (the default: alive while its slide is shown, gone when you leave), load, visible, idle, and none (never: the fallback is the whole story). slide is the one a deck wants — a deck keeps neighbouring slides mounted so transitions don’t stall, and hydrating all of them is how a phone runs out of memory on slide three.

Step a click and each island is handed new props through the renderer’s update. A chart keeps its animation and a half-typed form keeps its text, because the component was updated rather than rebuilt. Every framework does this differently — Vue’s reactive, Svelte’s $state, Solid’s store, Preact’s diff, Qwik’s signals — behind one optional method.

A renderer is an object with a name and a mount:

import type { DeckRenderer } from "@kadal/deck/renderers/contract";
export const myRenderer: DeckRenderer = {
name: "my-framework",
mount(host, component, props, context) {
// put the component into `host`
return {
update(next) {
/* new props */
},
destroy() {
/* take it out */
},
};
},
// optional: owns(component) — recognise a component by inspection; return false when unsure
// optional: ssr(component, props) — HTML for surfaces that never hydrate (print, thumbnails)
};

registerRenderer(renderer) adds one eagerly; registerLazyRenderer(name, () => import(…)) adds one by name, fetched the first time an island needs it. A renderer package must import @kadal/deck/renderers/contract — types, a registry and a symbol — never the main entry, which would drag React into a deck that never asked for it.

How the engine picks a renderer, in order: the framework the author named, then the stamp the build put on the component (Symbol.for("deck.renderer")), then owns(). Recognition is the build’s job, not the engine’s, because most frameworks’ production output is indistinguishable from a plain function.

React and custom elements ship with the engine. Each of the others is a package:

Framework Package Notes
Web components built in a tag name or a class
Vue @kadal/deck-vue options objects are recognised; functional ones say so
Svelte @kadal/deck-svelte a Vite plugin stamps compiled components
Solid @kadal/deck-solid *.solid.tsx + a pragma
Preact @kadal/deck-preact no plugin at all
Qwik @kadal/deck-qwik component$() is mandatory
Angular @kadal/deck-angular zoneless, JIT, no build plugin
Fresh @kadal/deck-fresh Preact under a Fresh name
Rust / WebAssembly @kadal/deck-wasm an ABI, not a framework
Python @kadal/deck-python CPython via Pyodide

The showcase deck runs all eleven on one slide, each fetched on arrival — none of them is in the deck’s first load.

Each renderer package has a ./build entry that imports nothing but the contract’s symbol. Import build helpers (deckSolidFiles, deckSveltePlugin, …) in vite.config.ts from there: a Vite config runs in Node, and importing the renderer itself would load the framework in Node — which some frameworks (Angular) cannot survive.