---
name: components-as-files
description: Write Obsidian UI components as single files against the components-as-files contract 0.2 (a default-exported function that receives ctx). Use when asked to build a dashboard widget, chart, tracker, visualiser or any small UI for an Obsidian vault.
---

# Writing a component

You are writing **one component**: one file that draws one piece of UI into a box and cleans up after itself. The full contract is in COMPONENTS.md; this file is everything you need to follow it.

## Do not (read this first)

1. Do not use `this.container`, `dv.container`, `document.body` or `document.querySelector`. Draw only inside `ctx.host`.
2. Do not use `dv`, `dv.pages`, `app` or any Dataview or Obsidian API. Use `ctx.query` and `ctx.navigate`.
3. Do not invent APIs. If a `ctx` member is not listed below, it does not exist. There is no `ctx.onunload`, `container.onunload`, `app.embedLoader` or `window.mermaid`.
4. Do not call `setInterval`, `setTimeout`, `requestAnimationFrame` or `addEventListener` on `window`/`document` directly. Use `ctx.interval`, `ctx.timeout`, `ctx.frame` and `ctx.listen`.
5. Do not write colours as hex, `rgb()` or names. Use CSS variables from the table below, or `ctx.palette` in canvas.
6. Do not write `px` lengths in CSS, except `1px` borders. Do not use `vw`, `vh` or media queries. Use `em`, `%`, `fr`, `cqi` and `@container`. Pixels are fine where they are not a design choice: SVG attributes, a canvas's pixel buffer, and positions you just measured (a tooltip placed at the point under the mouse).
7. Do not store anything on `window`, and do not use `localStorage`. Use `ctx.state` or `ctx.storage`.
8. Do not use element ids, and do not use bare class names like `.card` or `.title`. Two copies of a component can share a page, and the app's own styles reach into your box. Use classes prefixed with your component's name: `.heatmap-cell`, `.heatmap-title`.
9. Do not use `innerHTML` with any data. Build elements with `ctx.h`.
10. Do not write to the vault while rendering or from a timer.
11. Do not use `import`. The file must be self-contained.
12. Do not use placeholder or fake data when the component is about the user's notes. Read real notes with `ctx.query.notes()`.
13. Do not set a fixed width or height on the component, and do not use `height: 100%`. Your box's width comes from the host; its height comes from your content. Give a chart or canvas its height with `aspect-ratio` (and an `em` minimum if it needs one).
14. Do not change your box's size inside `ctx.onResize`, for example by setting a canvas's CSS height there. That triggers `ctx.onResize` again, in a loop. Set sizes in CSS once; in `ctx.onResize`, only redraw.
15. Do not build a URL for a vault file yourself. For an image, use `ctx.query.url(path)`.

## Output format

Reply with **exactly one fenced code block** containing the whole file, and nothing else unless you were asked for an explanation. Use `js` for a module component or `html` for an html component.

## How a component runs

- The host calls your default export **once per mount**, with a fresh, empty `ctx.host`.
- The host may **tear it down and mount it again** at any time: when the note changes, when the layout changes, or when the user switches notes. Every mount is a new call with a new `ctx`.
- On teardown, everything you registered through `ctx` is undone automatically: timers, frame loops, listeners, resize observers and styles. This is why rules 4 and 7 exist.
- `ctx.state` is the only thing that carries over between mounts. Put UI choices there (selected tab, filter).

## Template

```js
export const meta = {
  name: "Name",
  version: "1.0.0",
  description: "One sentence.",
  contract: "0.2",
  needs: ["vault-read"],            // declare what you use: vault-read, vault-write, navigate, app
  settings: { title: "Name" },      // every setting you read, with a default
};

export default function component(ctx) {
  const notes = ctx.query.notes({ folder: "Journal" });

  ctx.css(`
    .name-root { display: grid; gap: 0.5em; padding: 0.5em; }
    .name-title { font-weight: 600; color: var(--text-normal); }
    @container (min-width: 30em) { .name-root { grid-template-columns: 1fr 1fr; } }
  `);

  ctx.host.append(
    ctx.h("div", { class: "name-root" },
      ctx.h("span", { class: "name-title" }, ctx.settings.title),
      ctx.h("span", null, `${notes.length} notes`)));
}
```

## `ctx` reference (complete)

| Member | Use |
| --- | --- |
| `ctx.host` | Your box. Append into it. It is a size container: its width comes from the host, its height from your content. |
| `ctx.h(tag, props, ...children)` | Create an element. `props`: `class` (string or array), `style` (object), `on` (`{ click: fn }`), `dataset`, `aria-*`, any attribute. Children: nodes, strings, numbers, arrays; `null`, `false` and `true` are skipped. |
| `ctx.svg(tag, attrs, ...children)` | Same, for SVG elements. |
| `ctx.css(text)` | Add CSS. Your rules only apply inside your box. Other styles can still reach in, so prefix every class and `@keyframes` name with your component name. |
| `ctx.palette.color(name)` | A resolved colour for canvas: `text`, `muted`, `faint`, `accent`, `bg`, `bgAlt`, `border`, `red`, `orange`, `yellow`, `green`, `cyan`, `blue`, `purple`, `pink`. |
| `ctx.palette.series(n)` | `n` chart colours from the theme. |
| `ctx.palette.ramp(t)` | CSS colour for intensity `t` in 0..1 (heatmaps). |
| `ctx.palette.dark` | `true` in a dark theme. |
| `ctx.palette.onChange(fn)` | Redraw canvas when the theme changes. |
| `ctx.interval(fn, ms)` / `ctx.timeout(fn, ms)` | Timers, cleaned up for you. Each returns a `stop()` function if you need to cancel early. |
| `ctx.frame(fn)` | Animation loop: `fn(time)` every frame; return `false` to stop. |
| `ctx.listen(target, type, fn)` | Listener on something outside your host, removed for you. Listeners on your own elements need nothing special. |
| `ctx.onResize(fn)` | `fn({ width, height, em })` when your box changes size, in pixels; `em` is pixels per `em`, so `Math.floor(width / (2 * em))` is how many `2em` cells fit. It also fires once at the start. Never resize your box from here. |
| `ctx.teardown(fn)` | Anything else to undo. |
| `ctx.signal` | An `AbortSignal` for `fetch` and similar. |
| `ctx.query.notes({ folder, tag, where })` | Notes as plain objects (see below). |
| `ctx.query.text(path)` | `await` the text of one file: a note, or a data file. For JSON, `JSON.parse(await ctx.query.text(path))`. |
| `ctx.query.url(path)` | A URL for a file in the vault, for `<img src>`. Empty when the file does not exist: check before you set `src`, because `src=""` loads the page itself. |
| `ctx.note` | `{ path, frontmatter }` of the note you are in, or `null`. |
| `ctx.settings` | Your settings (read-only). |
| `ctx.state` | Object that survives re-mounts of this mount (same file, same note, same key). |
| `ctx.storage.get(key, fallback)` / `.set(key, value)` | Small JSON values that persist. |
| `ctx.navigate(path, { newTab })` | Open a note. |

A note from `ctx.query.notes()`:

```js
{ path, name, folder, tags /* ["a", "b/c"] */, frontmatter, created /* Date */, modified /* Date */, day /* "2026-03-04" or null */, size }
```

It is a normal array of normal objects: use `.filter`, `.map`, `.reduce` and `.sort` directly. Dates are JavaScript `Date` objects. `day` is set when the file name starts with a date. Frontmatter values are plain JSON; frontmatter dates are strings, so parse them with `new Date(value)`. Folder paths have no leading or trailing slash, and paths may contain spaces.

## Styling

Use these CSS variables. They exist in every Obsidian theme and in every conforming host.

| Purpose | Variables |
| --- | --- |
| Text | `--text-normal`, `--text-muted`, `--text-faint`, `--text-accent`, `--text-on-accent`, `--text-error`, `--text-success`, `--text-warning` |
| Surfaces | `--background-primary`, `--background-primary-alt`, `--background-secondary`, `--background-secondary-alt` |
| Lines and states | `--background-modifier-border`, `--background-modifier-hover`, `--background-modifier-active-hover` |
| Accent | `--interactive-accent`, `--interactive-accent-hover`, `--interactive-normal`, `--interactive-hover` |
| Colours | `--color-red`, `--color-orange`, `--color-yellow`, `--color-green`, `--color-cyan`, `--color-blue`, `--color-purple`, `--color-pink` |
| Type | `--font-interface`, `--font-text`, `--font-monospace` |
| Radius | `--radius-s`, `--radius-m`, `--radius-l` |

For a tint, mix: `color-mix(in srgb, var(--color-green) 20%, transparent)`. Shadows too: `box-shadow: 0 0.2em 0.6em color-mix(in srgb, var(--text-normal) 20%, transparent)`.

Sizes:

- Use `em` for spacing and type, `%`/`fr` for layout, and `cqi` for type that scales with the box: `font-size: clamp(1.5em, 12cqi, 4em)`.
- Change the layout with `@container (min-width: 30em) { … }`. The component should look right from `18em` wide upwards.
- Buttons in Obsidian have a default height and shadow. Reset them when you style your own: `height: auto; box-shadow: none;`.

## Canvas

Give the canvas its size in CSS, once: `width: 100%; aspect-ratio: 16 / 9;`. In `ctx.onResize`, set only its pixel buffer (`canvas.width = Math.round(width * devicePixelRatio)`, the same for height) and redraw; never set its CSS size there. Read colours with `ctx.palette.color()`, and read them again in `ctx.palette.onChange`, because colours you saved at mount are stale after a theme change. Animate with `ctx.frame`.

## Accessibility minimum

- Clickable things are `<button>` elements or have `role`, `tabindex="0"` and an Enter key handler.
- Every chart or grid has an `aria-label` that says what it shows in words.
- Anything conveyed by colour alone is also in the text or the label.
- Text contrast comes from the variables above; do not lower it with `opacity`.

## html components

Use an html component when the code should not have vault access, or when the user pastes a finished HTML page.

- Write a normal HTML fragment with `<style>` and `<script>`. It runs in a sandboxed iframe with no network.
- Read settings from `window.cf.settings` and data from `window.cf.data`. `window.cf.onTheme(fn)` fires on theme changes.
- The same CSS variables are available; use them, so it follows the theme. Ids and `px` are fine here, because every html component has its own document.
- Do not load anything from the network: no CDNs, fonts or images by URL. Inline everything.

## Before you answer, check

- [ ] One fenced block, whole file, no imports.
- [ ] `meta.settings` has a default for every setting read.
- [ ] Every timer, loop and outside listener goes through `ctx`.
- [ ] No hex, no `px` (except 1px borders), no `vw`/`vh`, no ids, no unprefixed class names, no `window` state.
- [ ] Works in a dark theme and a light theme without changes.
- [ ] Works at `18em` wide and at `60em` wide, with no fixed width or height.
- [ ] If it were mounted twice on the same page and re-rendered in the middle of an animation, nothing would leak or collide.
