Skip to content
sdocs

Prop Extraction

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

CategorySource
Propsinterface Props { … } + let { … } = $props()
EventsProps named on* whose type contains =>
SnippetsProps typed as Snippet or Snippet<[…]>
Methodsexport function foo() { … }
Statesexport 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 no Props interface and no JSDoc annotation, only prop names and defaults are extracted (no types).
  • Non-on* event propshandleClick: () => void is classified as a prop, not an event. Use the on* 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