.canvas — Directory Contract
Status: Draft V1 · App-agnostic format spec · Last updated 2026-06-16
A .canvas is a portable directory that holds one or more standalone documents
(SVG by default) plus a single manifest that describes how to interpret them. It is
a container format, not a scene format — it sits a layer above .grida and
SVG: a .grida file is one scene's IR; a .canvas is a folder of standalone
documents with an order and an optional 2D placement.
The manifest describes the bundle along two orthogonal axes: an editor
(editor — which editor opens the bundle: a linear slides deck or a freeform
board) and a content kind (files — which file patterns are documents,
e.g. SVG). They are independent: a deck of SVGs and a board of SVGs differ only
in editor; a deck of SVGs and a deck of some other document kind differ only
in files.
0. Philosophy — the only load-bearing part
- Reader-first, not writer-first. This spec defines how a tolerant reader
interprets a
.canvas, not how authors must write one. Like a code editor opening a folder: it reads whatever is there and does its best (Postel's law). - Minimally valid. Failure is nature. There are almost no MUSTs. An invalid,
partial, or weird
.canvasis a normal state of the world, not an error to reject. Readers degrade; they do not hard-fail. - The directory has no required shape. Humans and agents author it however they like. The spec constrains one file and nothing else.
- The manifest is the only authority.
.canvas.json(the godfile) is the single authored contract — the only file the reader parses. When it is present and parseable the reader follows it; absent or malformed, the reader degrades to implicit mode (§5), it does not reject. Every other file is taste — opaque to the format, never required, never validated. - 100% portable. It is a folder of files with relative references. Copy it, zip it, email the folder, check it into git. No database, no absolute paths, no host coupling.
1. What a .canvas is
A directory, conventionally suffixed .canvas (e.g. intro-deck.canvas/). The
suffix is a hint, not the contract.
- Marker: the directory contains a root file named
.canvas.json. Its presence — not the folder's name — is what declares the directory a.canvas. - Declared mode:
.canvas.jsonpresent → the reader follows it. - Implicit mode:
.canvas.jsonabsent or unreadable → a reader MAY still open the directory best-effort aseditor: "unknown", deriving content from the files it finds. (This is the "open any folder" behavior; it is optional and lossy.)
Marker name. The marker is
.canvas.json— hidden (a dotfile) and JSON-typed, so editor tooling and$schemastill apply, while staying unmistakable from JSONCanvas's single-file*.canvas. It is matched case-sensitively at the bundle root.
A directory is never invalid — at worst it is implicit.
2. The godfile — .canvas.json
JSON. The minimal valid manifest is {} — every field is optional and the reader
fills defaults. All paths are relative to the bundle root; .. escape and absolute
paths are out of scope for V1 (see §9).
Containment is the host's responsibility. A reader reconciles src against the
directory listing for existence only — it is not a security boundary and does not
reject ..-traversal or absolute paths. A consuming application that maps a src to a
real file MUST guard containment itself before any file operation.
{
// OPTIONAL. Spec version this manifest targets. Missing → reader assumes current.
"version": "1",
// OPTIONAL. Editor-tooling hint only. Ignored by readers.
"$schema": "https://grida.co/schema/dotcanvas/v1.json",
// OPTIONAL. EDITOR. "slides" | "board" | "unknown". Unrecognized/missing → "unknown". See §3.
"editor": "slides",
// OPTIONAL. CONTENT. The glob patterns whose matches are documents — and the
// file kind a host opens an editor for. Missing → ["*.svg"]. Empty [] derives
// nothing (rely on `documents`). Patterns match root basenames; `*` only. See §3.
"files": ["*.svg"],
// OPTIONAL. Explicit thumbnail pointer; overrides the filename convention in §4.
"thumbnail": "thumbnail.png",
// OPTIONAL. The ordered set of documents. ABSENT → reader derives from disk (§5).
// Array order IS the sequence order (the "slides view").
"documents": [
{
"src": "001.svg", // the only field that means anything; relative path
"id": "n_a1b2", // OPTIONAL stable id; absent → `src` is the identity
"layout": {
// OPTIONAL 2D placement (the "canvas view"); absent → no canvas position
"x": 0,
"y": 0,
"w": 1920,
"h": 1080,
"z": 0,
},
// OPTIONAL. Skip this document in the LINEAR slides view (it still EXISTS
// and shows in the canvas view); absent → not skipped. Advisory only.
"skip": false,
// NOTE: there is no `name`/`title` field — a human label is the document's
// own content's job (for an SVG slide, its `<title>` element).
},
],
// OPTIONAL. Vendor/app extension bag. Readers ignore keys they don't own; SHOULD round-trip them.
"ext": { "...": {} },
}
The two views from one list
This is the Figma-Slides duality (slides view + canvas view), expressed minimally:
- Slides view = the order of
documents[]. Each entry whosesrcis a document is rendered as exactly one slide, in array order. This view is whateditor: "slides"presents primarily. - Canvas view = each entry's optional
layout— where that same document sits on a 2D surface. Entries withoutlayoutsimply have no canvas position (the reader auto-places or omits them). This view is whateditor: "board"presents primarily.
One list, two projections. Both projections exist on every bundle regardless of editor;
editor only names which is the primary presentation. The canvas view is purely
additive: a slides document with no layout anywhere is still a perfectly valid linear
deck, and a board is the same list read through layout instead of order.
Skip (slides view). A document may carry "skip": true — omitted from the linear
slides view's running order while it still exists and shows in the canvas view. It is
skipped, not hidden: a non-linear viewer still sees it. Skip is advisory — the
reader round-trips it but does not drop skipped documents; honoring it is the slides UI's
job. This mirrors PowerPoint (sld@show), Google Slides (isSkipped), and
Keynote/Figma "Skip Slide".
No name/title. A human label is deliberately not a manifest field — it is the
document's own content's job (for an SVG slide, its <title> element), matching how
PowerPoint, Google Slides, and Keynote derive a slide's title from its content rather
than from deck metadata. The manifest governs order, placement, existence, and skip; the
label travels inside the document.
3. Editor (editor) and content (files)
A .canvas is described along two orthogonal axes. Editor is which editor opens the
bundle — how the documents are read/presented (à la Figma's editorType); content is
what they are. They are independent, so any editor can hold any content kind.
3a. Editor — editor
editor | Meaning |
|---|---|
"slides" | A linear deck. documents[] order is the primary presentation (the running order); layout is an additive canvas view. |
"board" | A freeform canvas. Each document's layout is the primary presentation (2D placement); order is secondary. |
"unknown" | The reader makes no assumption. Default for a missing/unrecognized editor and implicit mode. |
| (reserved) | All other strings are reserved for future editors. A reader that doesn't recognize an editor treats it as "unknown" — it never errors. |
3b. Content — files
files is the set of glob patterns whose matches at the bundle root are documents —
and, by the same token, the document file kind a host opens an editor for (*.svg →
an SVG editor).
- Missing →
["*.svg"]. SVG is the V1 default, so an SVG.canvasneeds nofiles. - Explicit
[]derives nothing — membership comes only fromdocuments[]. - Patterns match root basenames;
*(any run) is the only wildcard in V1. - It is advisory and tolerant: it drives disk-derivation (§5) and the host's editor choice, but a junk pattern simply matches nothing — never an error.
This is the seam that lets a future content kind join without a new editor: the editor
stays slides/board; only files changes.
4. Thumbnail (by convention)
A reader looks for a root file named thumbnail.png / thumbnail.svg / thumbnail.jpg
/ thumbnail.jpeg.
- All optional.
- If multiple exist:
pngwins, thensvg, thenjpg/jpeg. The others are a lint warning, not an error (failure is nature). - An explicit
thumbnailfield in.canvas.jsonoverrides the convention.
5. Reader semantics (the heart)
A conforming reader is tolerant by construction:
| Situation | Behavior |
|---|---|
.canvas.json missing | Open in implicit mode, editor: "unknown"; MAY derive a document list from on-disk files. |
.canvas.json is malformed JSON | Degrade to implicit mode + surface a warning. Do not hard-fail. |
Unknown top-level fields / unknown editor | Ignore (and SHOULD preserve on write). |
documents absent | Derive from disk: list root files matching files (default ["*.svg"]), excluding any reserved thumbnail cover (§4), order lexically by filename (the nnn.svg convention, §8). |
A documents[].src points at a missing file | Skip it with a warning. Disk wins. |
Disk has matching files not listed in documents | Reader MAY append them after the listed ones (disk wins for existence; manifest wins for order). |
Two entries share an id/src | Linter warning; reader keeps the first. |
The reconcile rule in one line: the manifest is authoritative for order and placement; disk is authoritative for existence.
Ordering is exactly documents order, then disk-only matches appended lexically —
there is no auto-renumber or re-sort mode (no "re-sequence to nnn.svg"), and there
won't be. Any renumbering is a consumer's own behavior, not the reader's.
6. Editor semantics (informative)
What a reader/editor does with a .canvas — descriptive, not normative:
- Reads
.canvas.json, renders documents in order (slides view) and optionally at theirlayout(canvas view). - Edits an individual document by rewriting its
srcfile in place. The document is a standalone SVG; editing it doesn't touch the manifest. - Reorder / move / show-hide → rewrites
.canvas.json. (Reorder = array order; move =layout.) - Two writers (e.g. an agent and a direct-manipulation editor) coordinate through the files on disk; last-write-wins per file is the floor.
7. Everything else is taste
Any file that isn't .canvas.json or a referenced document is opaque to the format:
plan.md,styles.css,theme.css,assets/, notes, scratch files, an app's private sidecars — none of these are part of the contract.- A tool MAY use them by its own private convention. The format neither requires, defines, nor validates them.
- A
.canvaswith nothing but.canvas.jsonis valid. A.canvascluttered with a hundred unrelated files is also valid.
8. Recommendations for writers (non-binding)
These are SHOULDs, offered so the format ages well — not gates:
- Preserve unknown fields on round-trip (don't destroy a newer writer's data).
- Relative paths only; keep the bundle self-contained.
- Stable-ish serialization (sorted keys, trailing newline) so
gitdiffs stay legible — nice for local-first, not required. - Prefer convention (
nnn.svg,thumbnail.png) when you have no reason to deviate, so the implicit-mode reader degrades gracefully. - Number slide files
001.svg,002.svg, … — 1-based, zero-padded. Page numbers start at 1, so a 1-based filename matches the slide number a viewer shows (file00N.svg≈ page N), which is friendlier than the 0-based000.svg. This is only a writer recommendation: the reader sorts lexically and is indifferent to the starting index (000.svgis still read fine). Nuance: once a slide is skipped (ordocuments[]reorders away from filename order), the file number and the visible page number diverge by the skipped count — that misalignment is inherent to any file-number scheme and is not a reason to 0-base or to avoid numbering.
9. Open questions (need an RFD before they're V2)
- External resources / links. Does a
.canvassupport symlinks or references outside the bundle (shared asset libraries, linked sibling.canvases)? Resolution rules, containment, and portability-when-copied are all unresolved. Deferred — needs its own RFD. This one is load-bearing for "100% portable": a.canvasthat symlinks out is no longer self-contained. - Stable identity vs path identity. V1 lets
srcbe the identity. Once rename + canvas-view + reorder all compose, opaqueids may need to be promoted from optional to recommended (move a file and everylayoutentry dangles). - Sealed transport. A zipped single-file form for sharing must not reuse the
.canvassuffix (that collides with single-file.canvasformats elsewhere, e.g. Obsidian's JSON Canvas). Naming TBD. Slides → general promotion.Resolved. The general editor iseditor: "board"(§3a), reached by the orthogonaleditor(application axis) andfiles(content) axes — exactly the reserved-editor+ additive-layoutmechanism this question anticipated. Further content kinds join viafiles, no new editor needed.
Relationship to other Grida formats
| Format | Layer | What it is |
|---|---|---|
| SVG | document | A single standalone graphic. The default document content (files: ["*.svg"]). |
.grida | scene IR | One scene's node graph (FlatBuffers). A single document's internal representation. |
.canvas | container | A portable directory of standalone documents + a manifest (order + optional 2D layout). |
.canvas does not replace or wrap .grida; it is a higher layer. A .canvas references
documents by relative path and stays agnostic about each document's internal format — V1
standardizes on SVG, but files reserves room for other document kinds.
Appendix A — Prior art & divergences (non-normative)
- JSONCanvas (Obsidian). Studied, not adopted. We borrowed the validated shape — a node = id + file ref + 2D box — and explicitly rejected its single-file container, its node+edge flowchart model (we have no edges in V1), and its silence on versioning/unknown-fields. Convergent where the problem forces it; divergent where its choices were weak. The name collision is tolerated because ours is a directory, theirs is a file.
- Figma family (
.fig/.jam/.deck/ …). The lesson we kept: one substrate, the editor as a profile (editor), so slides can be promoted to a general canvas without a format rename. - Sketch (
.sketchzip). The lesson: separate content from view-state. We go further — view-state (order/placement) is also document data, and the only validated file. - Freeform (opaque SQLite in an iCloud container). The anti-pattern. We are the transparent inverse: a portable folder of files.