The sdoc Language
Everything sdocs renders starts as a .sdoc file. A .sdoc file is a documentation format that contains Svelte: the file's structure is sdoc,
and Svelte code lives inside designated blocks.
File anatomy
Every .sdoc file has the same three-part shape, in this order:
<script>— at the top, optional. Imports and shared values, in JavaScript or TypeScript. Available to every entity in the file.- Entities — the middle. Any number of
[SHOWCASE],[DOC],[PAGE],[LAYOUT]and[PATTERNS]blocks, in any mix. Each entity is its own route (and usually its own sidebar entry). <style>— at the bottom, optional. CSS available to the file's previews and examples.
Only the entities are required — and a file doesn't have to be attached to a
component at all: About.sdoc can hold a single [DOC], Dashboard.sdoc a
single [LAYOUT], and a [SHOWCASE] block can even skip the preview and carry
only examples.
<script lang="ts">
import Button from './Button.svelte';
</script>
[SHOWCASE title="Forms / Button" description="A flexible button."]
[COMPONENT component={Button} args={{ label: 'Click me', disabled: false }}]
<Button {...args} />
[/COMPONENT]
[EXAMPLE title="With a very long label"]
<Button label="This label would never fit in an identifier" />
[/EXAMPLE]
[/SHOWCASE]
[DOC title="Forms / Button usage guide"]
## When to use a button
Buttons trigger actions. For navigation, use a link — see <Button label="the demo" />
rendered right here in prose.
[/DOC]
<style>
.demo-row { display: flex; gap: 8px; }
</style>The four entities
| Entity | Documents | Details |
|---|---|---|
[SHOWCASE] | components | live previews with controls (tabs when several), titled examples, extracted API |
[DOC] | freeform content | markdown-first, auto table of contents |
[PAGE] | a Svelte-built page | plain Svelte in the docs context — landing pages, custom routes |
[PATTERNS] | one composition | several components assembled — a user menu, a notifications system |
[LAYOUT] | a full-page sketch | rendered in an isolated frame |
The title attribute places each entity in the sidebar — plain path
segments separated by / create collapsible folders: title="Forms / Button" puts Button inside a Forms folder. Siblings at
every level sort alphabetically (pin items first with the section's order array). Two prefixes on the
first segment add structure above folders: : makes a bold sidebar group header (title=":Forms / Button"), and @slug/ assigns the
entity to a top-bar section declared in the
config (title="@components/Forms / Button"); an unknown slug is a
full-page error. The page shows the title's last segment as its heading.
Multiple entities per file
One file can hold several entities — a component's docs and its usage guide sharing the same imports, or a family of small related components. House rules:
- Name the file after its main entity (
Button.sdoc), and put that entity first. - The sidebar sorts siblings alphabetically regardless of file order — pin
a different order with the section's
orderarray. - If a file grows past a handful of entities, consider splitting it — the editor will nudge you.
The top <script> is shared source, not shared state: every preview and
example renders isolated, so each gets its own copy of anything mutable.
Blocks and attributes
- Tags are line-anchored square brackets:
[SHOWCASE …]opens,[/SHOWCASE]closes, each on its own line. Every tag is UPPERCASE — the entities, and the blocks inside them ([COMPONENT],[COMPONENTS],[EXAMPLE], and the text blocks). [COMPONENT]and[EXAMPLE]are also accepted lowercase, indefinitely: files written before 0.0.139 keep working, and the formatter capitalizes them when a document is next formatted.- Attributes use Svelte syntax:
title="text"for strings,component={Button}for expressions. Openers may span multiple lines. - Svelte code is valid only inside blocks. Body text outside any block is an error.
Terms used on this page
Each is defined once, here.
- Entity
- one
[SHOWCASE],[PATTERNS],[DOC],[PAGE]or[LAYOUT]— each is a route. - Block
- something written inside an entity, like
[COMPONENT]or[NOTES]. - Stage
- the isolated frame a preview renders in, with the project's own css.
- Flow
- what an entity shows, in the order it was written.
- Text block
- a block whose body is prose or a list rather than Svelte.
Text blocks
Four blocks carry text rather than markup. Three of them — [NOTES], [TODO] and [PROSE] — are UPPERCASE and alone on their line, with no
attributes and nothing else, which is what keeps a line opening with a
markdown link ([notes](/language/overview#text-blocks)) from scanning as a
block opener. The fourth, [GLOSSARY], takes attributes and so is matched
uppercase-only instead.
[NOTES]
Standing remarks about the thing being documented — one per line, once per
entity and once per [EXAMPLE]:
[SHOWCASE title="Forms / Button"]
[NOTES]
- bug: Focus ring lands 1px off in Safari.
- a11y: The icon-only variant has no accessible name.
- warning: Being replaced by ActionButton in v4.
- tip: Pair it with Field when it labels an input.
- Ships in the next release.
[/NOTES]
[COMPONENT component={Button}]
<Button {...args} />
[/COMPONENT]
[/SHOWCASE]Each line is - text, or - type: text. The type is one of bug, a11y, warning, perf, tip, info; leave it off and the note is a plain
remark, shown in grey. A type sdocs doesn't know is an error rather than a
silent fallback to grey, since grey is a real choice and a typo would look
like one.
A note is an observation — what should someone know about this thing.
Where a component sits in its life is status on the [COMPONENT], which is a property of it rather than a remark about
it. The two were one vocabulary until 0.0.140, and splitting them is what
lets a note say "being replaced by ActionButton" while the component itself
says deprecated.
Each note renders as an alert directly under the entity's title, in the order written.
Only a [SHOWCASE], a [DOC] and their [EXAMPLE] blocks hold text blocks. A [PAGE] or [LAYOUT] body is captured whole as Svelte markup,
so a block written in one is never read back — it would render as literal
text on the page. sdocs reports that (block-in-body) rather than letting
it through.
In the sidebar they become one dot in the entity's row: filled when the worst of them is on that page, hollow when it belongs to something inside it. A row always shows the worst note at or under it, ranked
bug → a11y → warning → perf → no type → tip → info
The axis is how much it costs the reader to not know. a11y sits under bug because it is a defect with a narrower blast radius, and above warning because it is still a defect rather than a caveat; perf sits
under warning for the same reason in the other direction. A note with no
type outranks tip and info deliberately — an author who didn't reach for
a word wasn't being reassuring.
So a folder of pages carries the loudest thing anywhere inside it. A hide entity's notes stay out of the roll-up: the mark would point at a row the
reader has no way to open.
[TODO]
A checklist, nested to any depth by indentation. Once per entity and once
per [EXAMPLE]:
[TODO]
- [x] Ship the component
- [ ] Document the dark theme
- [ ] Decide the token names
- [x] Pick the ramp
[/TODO]It always renders when present — this is documentation, not a dev-only
affordance. Under sdocs dev the whole list is editable: the boxes tick,
the + on its header adds an item, and clicking an item's text renames it
in place. Emptying an item removes it, and emptying the list removes the
block. Where an entity or example has no [TODO] at all, a button beside
the note button starts one.
Ticking writes straight back into the .sdoc, changing the single character
between the brackets and nothing else; only an edit that changes the list's
shape re-serializes the block. A built site shows the same list read-only,
since there is no source there to write to.
Indentation is meaning here rather than layout — it is what nests one item
under another — so the formatter leaves a [TODO] body exactly as written.
[GLOSSARY]
A titled list of terms, rendered where it is written. As many per entity as you like — a page can carry both "Terms" and "Abbreviations".
[GLOSSARY title="Terms" subtitle="Words this page uses" search]
- Stage: the isolated frame a preview renders in.
- Measure: the width of a line of text, in characters.
[/GLOSSARY]title sets the heading, subtitle a line under it, and search — a bare
flag — shows a filter box over the terms. Search is off by default: a
filter over four terms is furniture, and only the author knows whether the
list is four terms or forty.
The definition is inline markdown. The first colon splits the line, so a term may not contain one; a line that isn't a definition is an error rather than a silently dropped entry, and so is the same term twice.
In a [SHOWCASE] it flows with everything else in source order. In a [DOC] it sits mid-prose, exactly where you
wrote it — the same splice an [EXAMPLE] in a doc body already uses.
Its tag is the one text block matched uppercase only. The others take no
attributes, which is what lets them use the "nothing else on the line" rule;
a tag carrying a title can't, so the casing keeps [glossary](/some/link) a markdown link instead of a block opener.
[PROSE]
Markdown anywhere in a [SHOWCASE], as many times as you like: the
capabilities of a [DOC] body — fences, tables,
Svelte islands — without leaving the component's page.
[PROSE]
### Choosing a variant
Ghost buttons are for the third action in a row, never the first.
[/PROSE]Blocks render in the order they were written, with one exception:
several [COMPONENT] blocks are tabs over a shared stage, so the whole tab
strip is a single item wherever the first of them appears — see [COMPONENTS].
Inside an [EXAMPLE], [PROSE] is markdown only, and it follows the
example to its own route where a sibling block would not. A [DOC] takes no [PROSE] at all: its body is already prose, and a block nested in it would
have no answer to where it goes relative to the body around it.
Writing notes from the Explorer
Under sdocs dev a small note button sits at the end of every entity and
example title that can carry notes — a [SHOWCASE], a [DOC], or one of
their examples. It opens an editor for
that block's notes — add one, reword one, change a type, drop one — and Save writes straight back into the .sdoc file, which Vite reloads.
The edit is the smallest one that will do: only the [NOTES] block's own
span is rewritten, so the rest of the document keeps its formatting to the
byte, and emptying the list removes the block rather than leaving an empty
one behind.
It is a dev-server feature and cannot be otherwise: the endpoint behind
Save is mounted only by the dev server, and it refuses any file the project
does not already document. A built site has neither the button nor the
route. An embedded Explorer opts in by passing dev={import.meta.env.DEV}.