Skip to content
sdocs

Interactive Controls

Each [COMPONENT] block gets a live controls panel, and the tab you are on is the one it drives. Every control is chosen from the prop's TypeScript type — you write the type once and the panel follows.

Control types

Prop typeControlNotes
stringText input
numberNumber input
booleanCheckbox
'a' | 'b' | 'c' (string union)Select dropdownQuoted union members only
1 | 2 | 3 (number union)Select dropdownBare number union members only
CSS custom property — colorColor pickerDetected from @cssvar {color} JSDoc
CSS custom property — dimensionNumber + px unitDetected from @cssvar {dimension} JSDoc
Any other CSS custom propertyText input
Unsupported / complex typeRead-only displayShows the type, no editor

String union → select

If a prop is typed as a union of string literals, sdocs renders a select dropdown with one option per union member:

<script lang="ts">
  interface Props {
    size: 'sm' | 'md' | 'lg';
  }
  let { size = 'md' }: Props = $props();
</script>

In the controls panel, size becomes a dropdown with options sm, md, lg.

Rules for detection:

  • All members must be string literals wrapped in quotes ('sm', "md").
  • Members may be written across lines — the leading-pipe style Prettier wraps long unions into parses the same as a single line.
  • A mixed union falls back to a text box rather than a select, and it is a text box rather than a read-only row whenever the type mentions string at all — 'sm' | string is typable, 'sm' | number is not.

Number union → select

Same as string unions but for bare numbers:

interface Props {
  level: 1 | 2 | 3 | 4;
}

level becomes a dropdown with options 1, 2, 3, 4.

CSS custom property controls

For CSS variables, the control type is chosen from @cssvar JSDoc annotations, written one per line in a JSDoc block inside the <script>:

<script lang="ts">
  /**
   * @cssvar {color} --bg - Button background
   * @cssvar {dimension} --radius - Border radius
   */
</script>

<style>
  .button {
    background: var(--bg, #333);
    border-radius: var(--radius, 4px);
  }
</style>
  • {color} → color picker
  • {dimension} → number input with a px unit suffix
  • Any other {type} → plain text input

The annotation is what puts a variable in the panel at all — a var() in the component's <style> is not enough on its own, because a component references far more variables than a consumer is meant to override. What var() does supply is the default.

See prop extraction for the full extraction rules.

Default values

Initial control values come from the preview's args:

[COMPONENT component={Button} args={{ label: 'Click me', size: 'md', disabled: false }}]
	<Button {...args} />
[/COMPONENT]

A CSS custom property gets a control once it carries an @cssvar annotation; its default is read from the var() fallback in the component's <style> (var(--bg, #333)), so it rarely needs writing twice.

Set vs unset

A control distinguishes set (the prop is in args) from unset (the prop is absent and the component renders its own default):

  • An unset text or number input is empty, with the prop's default as ghost placeholder text. An unset select shows the default — or, with no default, a disabled Please select… entry.
  • An empty string is a real value, not unset: clearing an input keeps the prop set to "" (and the code shows name="").
  • Every set control gets a small after it: it unsets the prop, the attribute leaves the code, and the component falls back to its own default. A prop with no default to fall back to has nothing to unset to, and gets no ✕.
  • The same ✕ appears on a changed CSS custom property and returns it to its var() fallback — an unchanged var is never applied to the preview, so component defaults can't leak into nested components.

Reset

A Reset button restores every control to the preview's args, and CSS variables to their var() defaults. It sits in the Props header, or in the CSS Props header on a component that has editable variables and no props of its own.

Unsupported types

A prop whose type sdocs cannot classify — an imported interface, a complex generic, a union of object shapes — shows up in the panel as a read-only row: the name and the type, but no editor. A type merely containing string is typable, though, so Record<string, unknown> gets a text box rather than a read-only row.

This is intentional: rather than guessing and misrendering, sdocs surfaces the prop so you know it exists but skips the control.

See also