This file has a frontmatter header so Claude Code can load it as a skill. Other tools ignore it.
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

Template

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():

{ 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:

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

html components

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

Before you answer, check