Skip to content
sdocs

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:

  1. <script> — at the top, optional. Imports and shared values, in JavaScript or TypeScript. Available to every entity in the file.
  2. 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).
  3. <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

EntityDocumentsDetails
[SHOWCASE]componentslive previews with controls (tabs when several), titled examples, extracted API
[DOC]freeform contentmarkdown-first, auto table of contents
[PAGE]a Svelte-built pageplain Svelte in the docs context — landing pages, custom routes
[PATTERNS]one compositionseveral components assembled — a user menu, a notifications system
[LAYOUT]a full-page sketchrendered 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 order array.
  • 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

buga11ywarningperfno typetipinfo

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}.