Skip to content
sdocs

Component Docs

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

AttributeRequiredMeaning
titleyesSidebar path and display name — "Forms / Button" nests under Forms
slugnoOverrides the URL segment (default: slugified title segment)
hidenoA bare flag: routable, but never listed in a sidebar
descriptionnoShort text under the page title — inline markdown (`code`, **bold**, *italics*) renders styled
maxWidthnoContent column width (default from config, 1200px)
paddingnoDefault stage padding for this entity's previews and examples (default 16px)
directionnoDefault stage flex-direction (default row)
gapnoDefault stage gap (default 16px)
contentXnoDefault horizontal alignment: left/center/right/justify (default left)
contentYnoDefault vertical alignment: top/middle/bottom/justify (default top)
backgroundnoDefault stage background — a CSS color or a var() from the project's css
minHeightnoMinimum 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(…).)

AttributeRequiredMeaning
componentyesThe demonstrated component: an identifier imported in the file's <script>. Drives prop extraction and the controls.
argsnoThis preview's control defaults
titlenoTab label — defaults to the component's name
descriptionnoShort text shown above the stage — inline markdown renders styled
synonymsnoOther names this component answers to, comma-separated — see below
statusnoLifecycle marker on the tab: draft, wip, review, experimental, ready, deprecatedsee below
maxWidthnoStage width (default 100%; narrower stages center)
paddingnoStage padding — overrides the entity and config defaults
directionnoStage flex-direction — overrides the entity and config defaults
gapnoStage gap — overrides the entity and config defaults
contentXnoHorizontal alignment: left/center/right/justify
contentYnoVertical alignment: top/middle/bottom/justify
backgroundnoStage background — a CSS color or a var() from the project's css
minHeightnoMinimum 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]
ValueMeans
draftsketched, not real yet
wipbeing built
reviewbuilt, waiting on sign-off
experimentalusable, but the API may still change
readydone — use it
deprecatedon 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. args is 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: args and __sdocsRef in any script that feeds a preview stage (including the file script), __sdocsExample and a $props() call in scripts that feed a page body. [PAGE] scripts may use args freely — pages have no preview stage.
  • Styles apply within the block's own stage. In a [COMPONENT], a block script's component={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.