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 type | Control | Notes |
|---|---|---|
string | Text input | |
number | Number input | |
boolean | Checkbox | |
'a' | 'b' | 'c' (string union) | Select dropdown | Quoted union members only |
1 | 2 | 3 (number union) | Select dropdown | Bare number union members only |
CSS custom property — color | Color picker | Detected from @cssvar {color} JSDoc |
CSS custom property — dimension | Number + px unit | Detected from @cssvar {dimension} JSDoc |
| Any other CSS custom property | Text input | |
| Unsupported / complex type | Read-only display | Shows 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
stringat all —'sm' | stringis typable,'sm' | numberis 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 apxunit 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 showsname=""). - 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
- Prop extraction — how sdocs parses your component
- Writing component docs —
[COMPONENT]andargs