For each component documented in a .sdoc file, sdocs parses the component's source and extracts its public API. This page describes what's extracted and how to influence it.
What's extracted
| Category | Source |
|---|---|
| Props | interface Props { … } + let { … } = $props() |
| Events | Props named on* whose type contains => |
| Snippets | Props typed as Snippet or Snippet<[…]> |
| Methods | export function foo() { … } |
| States | export const x = $state(…) / $derived(…) |
| CSS custom properties | @cssvar annotations (defaults filled in from var() fallbacks) |
Each appears as its own section on the component's doc page. Sections with no entries are omitted, so a component's page shows only the categories it actually has.
Props
sdocs reads the Props interface and $props() destructuring together. Default values come from the destructuring:
<script lang="ts">
interface Props {
label: string;
size?: 'sm' | 'md' | 'lg';
disabled?: boolean;
}
let { label, size = 'md', disabled = false }: Props = $props();
</script>This extracts three props: label (string, no default), size (union with default 'md'), disabled (boolean with default false).
Naming the props type
The interface doesn't have to be called Props — sdocs reads whichever type the $props() declaration is annotated with, so let { label }: ButtonProps = $props() works the same way.
Defaults come from the destructuring
Binding $props() to a name instead of destructuring is valid Svelte, but it puts the defaults somewhere static analysis can't follow:
<script lang="ts">
const defaults = { size: 'md' } as const satisfies Partial<ButtonProps>;
let props: ButtonProps = $props(); // types still read; defaults do not
</script>The props themselves still come through — names, types, descriptions, and optionality all live on the interface — but every default shows as none. get_component_api and scaffold_component_doc return a warnings array saying so, rather than letting a table of empty defaults pass for an answer. Destructure to document defaults.
JSDoc comments on interface members are picked up as descriptions — the whole comment, with hard-wrapped lines rejoined (list items keep their breaks), and inline markdown (`code`, **bold**, *italics*) rendered styled:
interface Props {
/** The button label text */
label: string;
}class and ...rest forwarding
The standard forwarding shape — a merged class and a rest spread on the
root element — is recognized and kept out of the props table:
<script lang="ts">
import type { HTMLAttributes } from 'svelte/elements';
interface Props extends HTMLAttributes<HTMLDivElement> {
label: string;
}
let { label, class: className, ...rest }: Props = $props();
</script>
<div class={['Box', className]} {...rest}>{label}</div>class and ...rest aren't component API — they forward whatever the
consumer passes to the root element — so instead of rendering as (falsely
required) prop rows, they appear as small chips under the table: class,
and …rest labeled with the extended type when the interface declares one
(here HTMLAttributes<HTMLDivElement>). Detection reads the $props() destructuring itself, so it works the same in TypeScript, plain JS, and
JSDoc-typed components.
Plain JS components (JSDoc)
Components without TypeScript get the same extraction through JSDoc. Two forms
are supported on the $props() declaration:
An inline object annotation (types and optionality):
<script>
/** @type {{ label?: string, tone?: 'info' | 'success' }} */
let { label = 'Badge', tone = 'info' } = $props();
</script>Or @typedef with @property tags — this form also carries descriptions:
<script>
/**
* @typedef {Object} Props
* @property {string} [label] - Badge text
* @property {'info' | 'success'} [tone] - Color tone
* @property {() => void} [ondismiss] - Called when dismissed
*/
/** @type {Props} */
let { label = 'Badge', tone = 'info', ondismiss } = $props();
</script>Bracketed names ([label]) mark a prop optional. Defaults still come from the
destructuring. Event and snippet classification work the same as in TypeScript
(including import('svelte').Snippet).
Events
A prop is classified as an event when:
- Its name starts with
on(e.g.onclick,onchange,onsubmit) - Its type contains
=>(i.e. it's a callback)
interface Props {
onclick?: (e: MouseEvent) => void; // → event
onchange?: (value: string) => void; // → event
oneTime?: boolean; // → prop (starts with "on" but the type isn't a function)
}Snippets
A prop is classified as a snippet when its type is Snippet or Snippet<[…]>:
import type { Snippet } from 'svelte';
interface Props {
children: Snippet;
item: Snippet<[value: string, index: number]>;
}The parameter tuple is preserved so it appears in the docs.
Methods
Exported functions:
<script lang="ts">
/** Clears the input value */
export function clear(): void {
value = '';
}
</script>JSDoc comments become the method description.
States
Exported $state and $derived variables:
<script lang="ts">
export const count = $state(0);
export const double = $derived(count * 2);
</script>CSS custom properties
A CSS variable is part of the documented API when you say so, with an @cssvar annotation in a JSDoc block inside the <script> — one per line:
<script lang="ts">
/**
* @cssvar {color} --bg - Button background
* @cssvar {dimension} --radius - Corner radius
*/
</script>{type} picks the control (color a swatch, dimension a number and
unit, anything else a text box), the description runs to the end of the
line, and an optional trailing (default: …) sets the default.
Annotation is deliberate rather than automatic. A component's <style> usually references far more variables than a consumer is meant to
override — the design system's own tokens, internal wiring, one-offs — and
a table listing all of them documents the implementation instead of the
API. The annotated ones are the contract.
Where the default comes from
sdocs fills in the default from the style itself, so the annotation rarely
needs one: a --x: value declaration on the component's root wins, and
otherwise the fallback in var(--x, fallback), when every use of the
variable agrees.
.button {
background: var(--bg, #333);
padding: var(--padding, 8px 16px);
}An annotated --bg shows #333 here without repeating it. When different
properties use different fallbacks for the same variable, the Default
column shows Mixed — hover it for the per-property breakdown. A (default: …) on the annotation overrides all of this.
The supported types are color (→ color picker) and dimension (→ number + px unit). See interactive controls.
Without the annotation, the var is still extracted and gets a plain text input.
Limitations
- Untyped
$props()— with noPropsinterface and no JSDoc annotation, only prop names and defaults are extracted (no types). - Non-
on*event props —handleClick: () => voidis classified as a prop, not an event. Use theon*naming convention. - Complex TypeScript types — conditional types, deep generics, or types imported from other files may not be fully understood. They show up in the props table but may not get the best control.
- External types — if a prop references a type from another file, only the type name is shown; sdocs doesn't follow the import.
See also
- Interactive controls — how extracted props become UI controls
- Writing component docs