The component contract
Version 0.2. This document is the whole idea: if you can write a file that follows it, any host that implements it can run your component, and any model that reads SKILL.md can write one.
1. What a component is
A component is one self-contained file that draws one piece of UI into a box it is given, and cleans up after itself when the box goes away.
There are two kinds:
| Kind | File | Runs as | Can touch the vault | Use it for |
|---|---|---|---|---|
| module | name.js |
An ES module in the host's page | Yes, through ctx |
Anything that reads your notes |
| html | name.html |
A document in a sandboxed iframe, no network | No | Code you did not write and have not reviewed, pasted AI artifacts, self-contained tools |
A module component looks like this:
export const meta = {
name: "Clock",
version: "1.0.0",
description: "Current time, updating every second.",
settings: { seconds: true },
};
export default function clock(ctx) {
const time = ctx.h("div", { class: "clock-time" });
ctx.host.append(time);
ctx.css(`.clock-time { font-size: 2em; color: var(--text-normal); }`);
const tick = () => {
time.textContent = new Date().toLocaleTimeString([], {
timeStyle: ctx.settings.seconds ? "medium" : "short",
});
};
tick();
ctx.interval(tick, 1000);
}
That is the entire format: an optional meta export and a default-exported function that receives ctx. The function may be async. It may return a cleanup function, although registering cleanup through ctx is preferred.
A component file:
- has no imports. Everything it needs comes from
ctxor the platform (DOM,Intl,Math,Date,crypto.randomUUID, canvas). - uses no globals: not
window.*state, notdocument.body, notlocalStorage, notappdirectly. - keeps data out of code. Anything a user might change goes in
meta.settingsand is read fromctx.settings.
2. meta
All fields are optional. Hosts show name and description wherever a component is picked or trusted.
| Field | Type | Meaning |
|---|---|---|
name |
string | Human name |
version |
string | Semver of the component itself |
description |
string | One sentence |
settings |
object | Default settings. Every key a component reads from ctx.settings should have a default here. |
needs |
string[] | What it uses, for disclosure: "vault-read", "vault-write", "navigate", "app". Hosts show this when asking for trust. It is a declaration, not a sandbox (see Trust). |
contract |
string | Contract version it was written for, currently "0.2" |
3. The ctx object
Every member is optional for a host to enhance, but every member listed here must exist, so a component never has to feature-test.
Drawing
| Member | Signature | Notes |
|---|---|---|
ctx.host |
HTMLElement |
The component's box. Empty when the function is called. Append into it; never reach outside it. It is a size container (container-type: inline-size), so @container queries and cqi units work. Its width comes from the host; its height does not: it is as tall as what you put in it. So nothing in it can be height: 100% of it. Give charts and canvases a height through aspect-ratio, or an em height. |
ctx.h |
(tag, props?, ...children) => Element |
Creates an element. props may hold class, style (an object, custom properties allowed), on (an object of event listeners), dataset, and any attribute. Children may be nodes, strings, numbers, arrays, null, false or true; the last three are skipped. |
ctx.svg |
(tag, attrs?, ...children) => SVGElement |
Same, in the SVG namespace. |
ctx.css |
(text) => void |
Adds a style sheet scoped to this component. Selectors are nested under the host automatically, so .clock-time { … } only matches inside this component. Removed on teardown. Scoping keeps your rules in, not other rules out: prefix class names with the component's name. |
Theme
| Member | Signature | Notes |
|---|---|---|
ctx.palette.dark |
boolean |
Current theme mode. Kept up to date. |
ctx.palette.color |
(name) => string |
A resolved colour string (rgb(…)), for canvas and computed styles. Names: text, muted, faint, accent, bg, bgAlt, border, red, orange, yellow, green, cyan, blue, purple, pink. |
ctx.palette.series |
(n) => string[] |
n distinguishable theme colours for charts. |
ctx.palette.ramp |
(t) => string |
A CSS colour for intensity t between 0 and 1, from background to accent. For DOM and SVG. |
ctx.palette.onChange |
(fn) => void |
Called after the theme or accent changes. Unsubscribed on teardown. |
In CSS, prefer the host's own variables directly (var(--text-normal), var(--background-secondary), var(--interactive-accent)). They update with no code at all. ctx.palette is for the places CSS cannot reach, mainly canvas.
Lifecycle
Everything registered here is undone automatically on teardown, in reverse order.
| Member | Signature | Notes |
|---|---|---|
ctx.teardown |
(fn) => void |
Runs fn when the component is removed or re-rendered. |
ctx.interval |
(fn, ms) => stop |
setInterval that is cleared on teardown. Call stop() to clear it earlier. |
ctx.timeout |
(fn, ms) => stop |
setTimeout that is cleared on teardown. Call stop() to cancel it earlier. |
ctx.frame |
(fn) => stop |
Calls fn(timestamp) on every animation frame until teardown, or until fn returns false. Paused while the host is off screen. |
ctx.listen |
(target, type, fn, options?) => stop |
addEventListener that is removed on teardown. Needed only for targets outside ctx.host, such as document. |
ctx.onResize |
(fn) => void |
Calls fn({ width, height, em }) once when the host is first laid out, and again whenever its size changes. Sizes are in CSS pixels; em is the pixels in one em at the host's font size, for fitting a whole number of em-sized cells. Do not change the host's size from inside fn (for example by setting a canvas's CSS height): that calls fn again, in a loop. |
ctx.signal |
AbortSignal |
Aborted on teardown. Pass it to fetch or to your own listeners. |
Data
| Member | Signature | Notes |
|---|---|---|
ctx.query.notes |
(filter?) => Note[] |
Notes from one cached snapshot of the vault, shared by every component on the page. Filter by folder, tag or a where(note) function. Returns plain arrays of plain objects (see below). |
ctx.query.text |
(path) => Promise<string> |
The text of one file: a note, or a data file such as JSON or CSV. Empty if there is no such file. |
ctx.query.url |
(path) => string |
A URL for a file in the vault, for <img src>, <audio> and similar. Each host makes its own kind of URL; never build one yourself. Empty if there is no such file, so check it before setting src: an empty src loads the page itself. |
ctx.note |
{ path, frontmatter } \| null |
The note this component is mounted in, if any. |
ctx.settings |
object |
meta.settings merged with the settings given at mount time. Read-only. |
ctx.state |
object |
A plain object that survives re-renders of the same mount, but not a restart. A mount is this file, in this note, with this key; two mounts never share state. Use it for "which tab was selected". |
ctx.storage |
{ get(key, fallback), set(key, value) } |
Small JSON values that persist, scoped to this component and mount. |
A Note is:
{
path: string; // "Journal/2026-03-04.md"
name: string; // "2026-03-04"
folder: string; // "Journal"
tags: string[]; // ["journal", "health"], no leading "#"
frontmatter: object; // plain values, dates left as strings
created: Date;
modified: Date;
day: string | null; // "2026-03-04" when the name starts with a date, else null
size: number; // bytes
}
Plain arrays and Date objects, on purpose. No proxy arrays, no library date types.
Host
| Member | Signature | Notes |
|---|---|---|
ctx.navigate |
(path, { newTab? }) => void |
Opens a note. |
ctx.runtime |
string |
Which host is running this: "dataviewjs", "web", or a host's own name. For display and debugging only; do not branch behaviour on it. |
ctx.app |
object \| undefined |
The raw host application (the Obsidian App), only when meta.needs includes "app". An escape hatch. A component that uses it only works in that host. |
4. Lifecycle
- Mount. The host creates an empty
ctx.host, buildsctx, and calls the default export once. - Re-render. Some hosts re-run components when data changes, and a component cannot know when. DataviewJS, for example, re-runs every visible block a few seconds after any note in the vault changes, not only the note the block is in, and it does so without unloading the previous run. Whatever the host's runtime does, the host must make a re-render a full teardown followed by a fresh mount with a new
ctx.host. That is the host's job, so a component never needs to detect a re-render.ctx.statecarries over; nothing else does. - Teardown. Everything registered through
ctxis undone, then any function the component returned is called.
The one test every component must pass: mounted twice on the same page and re-rendered in the middle of an animation, nothing leaks and nothing collides. No timers left running, no listeners left behind, no element ids shared between the two copies, no style bleeding from one into the other.
5. html components
An html component is a complete HTML document (or fragment) run inside an iframe with sandbox="allow-scripts" and a content security policy that blocks all network access. It cannot see the vault, the host page, cookies or storage.
Before the file runs, the host defines window.cf:
| Member | Meaning |
|---|---|
cf.settings |
The settings given at mount time |
cf.data |
Any data the host chose to pass in at mount time (for example, a list of note titles). Plain JSON. |
cf.dark |
Current theme mode |
cf.onTheme(fn) |
Called when the theme changes |
The host also sets the same CSS variables used in the host application (--text-normal, --background-primary, --interactive-accent and so on) on the iframe's root element, keeps them in sync, and sizes the iframe to its content.
This is the kind to use for code from a model or a person you do not trust, and it is how a pasted AI-generated HTML artifact becomes a component.
An html component is its own document, so the rules that protect a shared page do not apply inside it: ids and px are fine. The rules about the theme do: use the CSS variables, so it looks right in both themes. And it cannot use the network.
6. Trust
A module component runs with the same privileges as the host page. In Obsidian that means it can read and write any file in the vault. meta.needs states intent; it does not limit anything. The rules below exist because of that.
Hosts must:
- Never run a component the user has not explicitly mounted.
- Make the source one step away from the rendered component.
- Run html components sandboxed as described in section 5.
Hosts should:
- Support pinning: the user records a hash of the file they reviewed, and the host refuses to run it if the file changes, showing the new hash and the source instead. This matters most in synced vaults, where a file can change without you touching it.
- Show
meta.needsand a plain "runs JavaScript with access to your vault" notice for module components.
Users should read a component before mounting it, or have an AI read it for them with a prompt such as: "Review this component for anything that reads or writes files, touches the network, runs shell commands, or uses globals. List every such line."
7. What a host provides
A host is anything that can give a component a box and a ctx: a DataviewJS block, a web page, an Obsidian plugin with a layout grid. To conform to contract 0.2, a host:
- implements every member in section 3 (members it cannot support return empty values rather than being absent),
- follows the lifecycle in section 4,
- scopes
ctx.cssto the host element, - follows the trust rules in section 6,
- supports html components as described in section 5, or refuses them clearly.
Two hosts exist so far, both in cf.js: a DataviewJS host for Obsidian (mount), and the generic run and runHtml that the article page uses to run the examples.
8. File conventions
- One component per file. Name the file after the component:
clock.js,heatmap.js. - A folder is fine when a component has an example or a README beside it:
clock/clock.js. - Keep components small. Past about 300 lines, split the idea into two components.
- Settings that hold lists or records are better kept in the host note's frontmatter and passed in, so the data lives with your notes and not in code.
Changes
- 0.2 (2026-10-15). Added
ctx.query.urlfor images and other files, andeminctx.onResize. Stated that the host's height comes from its content, and that resizing the host insidectx.onResizeloops. Said which rules apply inside html components. A 0.1 component runs unchanged on a 0.2 host. These changes came from having a model write fourteen components from this document alone: it got stuck on images, two components grew without limit or redrew forever, and it had to readcf.jsto learn what this page left out.