A [SHOWCASE] block documents components: live previews wired to interactive
controls, frozen examples, and each previewed component's extracted API.
<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="Disabled"]
<Button label="Can't touch this" disabled />
[/EXAMPLE]
[/SHOWCASE][SHOWCASE] attributes
| Attribute | Required | Meaning |
|---|---|---|
title | yes | Sidebar path and display name — "Forms / Button" nests under Forms |
slug | no | Overrides the URL segment (default: slugified title segment) |
hide | no | A bare flag: routable, but never listed in a sidebar |
description | no | Short text under the page title — inline markdown (`code`, **bold**, *italics*) renders styled |
maxWidth | no | Content column width (default from config, 1200px) |
padding | no | Default stage padding for this entity's previews and examples (default 16px) |
direction | no | Default stage flex-direction (default row) |
gap | no | Default stage gap (default 16px) |
contentX | no | Default horizontal alignment: left/center/right/justify (default left) |
contentY | no | Default vertical alignment: top/middle/bottom/justify (default top) |
background | no | Default stage background — a CSS color or a var() from the project's css |
minHeight | no | Minimum stage height, so a short preview still has room |
A [SHOWCASE] also takes the three text blocks — [NOTES], [TODO] and [PROSE] — which are blocks in its body rather than attributes on its
opener.
[COMPONENT]
A [COMPONENT] block is a live, interactive showcase. Each one names the
component it demonstrates and declares the defaults for its interactive controls. (The
block resolves a compound sub-component too — component={NavTree.Item} —
so a single import NavTree from './index.js' documents the whole family.
The index module is read as TypeScript, so the root resolves through export default, export { X as default }, Object.assign(Root, …), and
type-annotated forms like const Nav: typeof Root & { Item: typeof Item } = Object.assign(…).)
| Attribute | Required | Meaning |
|---|---|---|
component | yes | The demonstrated component: an identifier imported in the file's <script>. Drives prop extraction and the controls. |
args | no | This preview's control defaults |
title | no | Tab label — defaults to the component's name |
description | no | Short text shown above the stage — inline markdown renders styled |
synonyms | no | Other names this component answers to, comma-separated — see below |
status | no | Lifecycle marker on the tab: draft, wip, review, experimental, ready, deprecated — see below |
maxWidth | no | Stage width (default 100%; narrower stages center) |
padding | no | Stage padding — overrides the entity and config defaults |
direction | no | Stage flex-direction — overrides the entity and config defaults |
gap | no | Stage gap — overrides the entity and config defaults |
contentX | no | Horizontal alignment: left/center/right/justify |
contentY | no | Vertical alignment: top/middle/bottom/justify |
background | no | Stage background — a CSS color or a var() from the project's css |
minHeight | no | Minimum stage height, so a short preview still has room |
args values are plain literals — strings, numbers, booleans, and null.
They stay simple because the controls send them into the isolated preview at
runtime.
Anything richer (arrays, objects, imported values) belongs directly in the
body markup, where full Svelte is available.
Inside the body, args is in scope — spread it, pick from it, or ignore it.
Two descriptions, and which is which
A component's tab can carry two lines of prose, answering different questions:
<!--
@component
A **button**. Use it for actions; for navigation, use a link.
-->
<script lang="ts">
let { label }: { label: string } = $props();
</script>[COMPONENT component={Button} description="This preview starts disabled — flip the control."]
<Button {...args} />
[/COMPONENT]The .svelte file's <!-- @component --> comment describes the
component, wherever it appears. That's Svelte's own convention — your
editor already shows it on hover — so it belongs beside the code, written
once, rather than repeated in every .sdoc that previews it. Markdown and
code fences work inside it, and sdocs renders them.
The block's description="…" describes this preview: what it is set up
to show, what to try. Inline markdown only.
When both exist, both render — the block's line first, the component's own
underneath, then the stage. The MCP get_component_api tool returns the @component text too, so an agent reading a component's API sees what it is
for and not only what props it takes.
status — where a component is in its life
Optional, and shown as a glyph after the name on the component's tab — which is why the tab strip renders even when there is only one component to show.
[COMPONENT component={Button} status="experimental"]
<Button {...args} />
[/COMPONENT]| Value | Means |
|---|---|
draft | sketched, not real yet |
wip | being built |
review | built, waiting on sign-off |
experimental | usable, but the API may still change |
ready | done — use it |
deprecated | on the way out |
One linear path, plus the end of it. review and experimental are both
"not final", and the difference is who they wait on: a review waits on a
person, an experiment waits on real use.
Leave it off and the component has no status, which reads as nobody said —
a different thing from draft. A value sdocs doesn't know is an error rather
than a silent no-status, for the same reason an unknown note type is.
This is a property of the component. A remark about it — including "use
ActionButton instead" — is a [NOTES] line, which can carry the text that says what to use.
synonyms — the component's other names
A component is rarely known by one name. synonyms lists the others:
[COMPONENT component={Badge} synonyms="pill, chip, tag"]
<Badge {...args} />
[/COMPONENT]They render as quiet chips above the stage — a footnote to the description,
not a heading — and they feed search_docs, so an agent asked for "a chip"
finds the Badge that is one. Spacing, a trailing comma and a repeated name
all wash out, so "pill,chip," and "pill, chip" mean the same thing.
Multiple previews — tabs
A [SHOWCASE] block holds any number of previews. With one, the page is a plain
component page. With several, the page grows a tab bar: each tab is that
preview with its own controls and its component's API tables — every tab
fully live.
[SHOWCASE title="Navigation / Tabs"]
[COMPONENTS]
[COMPONENT component={Tabs} args={{ active: 0 }}]
<Tabs {...args}>
<Tab label="One">…</Tab>
<Tab label="Two">…</Tab>
</Tabs>
[/COMPONENT]
[COMPONENT component={Tab} args={{ label: 'One' }}]
<Tabs>
<Tab {...args}>…</Tab>
</Tabs>
[/COMPONENT]
[/COMPONENTS]
[/SHOWCASE][COMPONENTS] — why the wrapper
A lone [COMPONENT] needs no container. Two or more must sit inside one [COMPONENTS], and sdocs reports it if they don't.
The reason is what tabs are: several previews share one stage, one code
panel and one API table, so they are a single item on the page, not two
things in a row. Once a [SHOWCASE] can also hold [PROSE], that distinction has to be written
down — with two bare components and a paragraph between them there is no
correct place for the tab strip they share, and refusing beats guessing.
With the container, everything else in the entity flows in the order you wrote it, and the tab strip renders where the container sits.
One container per [SHOWCASE], holding [COMPONENT] blocks only.
Tab labels default to the component name (Tabs, Tab above); set title="…" on a preview to override it — which is also how two previews of
the same component stay distinguishable.
This is made for components that belong together: compound families like Tabs/Tab or Select/Option whose children never stand alone (wrap the
child in its parent inside the body, as above), or a component and its close
twin. Unrelated components read better as separate [SHOWCASE] blocks — one
file can hold several.
A block with zero previews is valid too: an examples-only page, with no controls and no API tables.
[EXAMPLE]
Examples are frozen showcases: each renders exactly what you wrote,
always — the controls never touch them. Every example requires a title (any text — spaces and punctuation welcome), unique within its [SHOWCASE] block. maxWidth, padding, direction, gap, contentX, contentY, background and minHeight tune its stage, like on [COMPONENT]; description adds a short text under the heading. An example also takes its own [NOTES], [TODO] and [PROSE] blocks (see text blocks) — nested
inside its body, where the parser lifts them out before anything is staged.
They are the only things that follow the example to its own route: a sibling
block belongs to the page, not to the example.
code="false" hides the example's code panel — useful in a [DOC], where an example often illustrates the prose
rather than showing how it is built:
[EXAMPLE title="In a user menu" code="false"]
<Avatar /><Badge count={3} />
[/EXAMPLE]The panel shows by default, and a value that is neither "true" nor "false" is an error rather than a silent no.
Examples belong to the page, not to a tab: they render below the preview
area and stay visible whichever tab is active. The same block also works
inside a [DOC] — there it renders in place,
mid-prose.
tags — what an example shows
tags marks the parts an example puts on screen, or the context it belongs
to:
[EXAMPLE title="In a user menu" tags="user menu, avatar, badge"]
<Avatar /><Badge count={3} />
[/EXAMPLE]Like synonyms, they render as quiet chips — here under the description —
and they feed search_docs. Tag every example that
composes a user menu and one query collects them from across the project,
whichever component's page they live on.
The stage is the container — don't wrap its content
A preview or example stage is itself a flex container: display: flex with flex-wrap: wrap, and flex-direction, gap, justify-content and align-items set from its attributes. Content goes straight into the
block, and a wrapper <div> only fights it.
[EXAMPLE title="Sizes"]
<div style="display: flex; gap: 8px; height: 500px; overflow-y: auto;">
<Badge text="sm" />
<Badge text="md" />
</div>
[/EXAMPLE]Every part of that wrapper is already handled — the flex row and gap by the stage, the height and scrolling by the frame, which measures its content, grows to fit, and scrolls on its own past 800px:
[EXAMPLE title="Sizes" direction="row" gap="8px"]
<Badge text="sm" />
<Badge text="md" />
[/EXAMPLE]The one height knob worth reaching for is minHeight, and it does the
opposite of a scroll box: it reserves room, so content that overflows
the stage — an open dropdown, a popover — shows instead of being clipped.
Scoped <script> and <style> — the full ladder
One rule, every level: any container can open with a <script> and
close with a <style>, and scopes nest lexically — file → entity →
block. The file script is visible everywhere; an entity's script (placed
right after the [SHOWCASE]/[DOC]/[PAGE]/[LAYOUT] opener) is
visible to that entity's blocks and body; a block's script only inside that
block. Styles cascade the same way into the stages. Two examples can each
declare their own let active = $state(…) without colliding, and shared
demo data can live once at the entity level.
[EXAMPLE title="Driven by data"]
<script lang="ts">
const items = [
{ label: "Introduction", href: "#intro" },
{ label: "Components", href: "#components" },
];
let active = $state("Introduction");
</script>
<NavTree>
{#each items as item (item.label)}
<NavTree.Item
label={item.label}
active={active === item.label}
onclick={() => (active = item.label)}
/>
{/each}
</NavTree>
<p class="current">Active: {active}</p>
<style>
.current { font-size: 12px; }
</style>
[/EXAMPLE]The rules:
- The block
<script>must be the first content of the body, the<style>the last (Format Document keeps them there). - Block scripts are real scripts — imports,
$state, functions, anything.argsis in scope in preview scripts. - Re-importing something the file's
<script>already imports is an error: it's already in scope. - Declaring a name the file script also declares is a compile error — the scopes nest, they don't shadow.
- A few names belong to the machinery and are diagnosed wherever they'd
collide:
argsand__sdocsRefin any script that feeds a preview stage (including the file script),__sdocsExampleand a$props()call in scripts that feed a page body.[PAGE]scripts may useargsfreely — pages have no preview stage. - Styles apply within the block's own stage. In a
[COMPONENT], a block script'scomponent={X}import may also live in the block's script.
What sdocs extracts
For every previewed component, sdocs parses its source and extracts the full public API — props, events, snippets, methods, states, and CSS custom properties. See prop extraction.