defuss-shadcn / Guides / state-api
State API
Interactive components don't just look stateful - they declare their states. Every component that ships a .js file exposes its states by name, so agents, tests, and your own code can drive and observe any documented state without knowing the implementation.
On this page (9)
§The contract
Each component instance binds a tiny API directly on the element. State lives on the element (never in module scope), so twenty components on one page can each hold a different state:
// the element - df$ selects it (get(0) = the node itself)const el = df$('#confirm').get(0);// set a declared state by name - throws on unknown namesel.api.setState('open', { /* optional config */ });// read it back - always reflects the real UIel.api.getState(); // → { name: 'open', config: { ... } }Three guarantees, verified by the build's verifier for every component:
§Declared, not implied
Each component lists its states as names (const dialogStates = ['default', 'open']) - default first. Unknown names throw with the supported list.
§Honest reads
getState() reflects what the user sees: pressing Escape on an open dialog returns default, not the stale last setState value.
§Documented visually
Every state is a named entry in the component's skill (## States), a section on its doc page, and a real screenshot in both light and dark mode.
§Try it
The buttons below call api.setState() and print what api.getState() reads back. Closing the dialog with Escape or the backdrop flips the observed state to default on its own - the read always matches the UI.
§Drive the dialog by name
The same API keyboard users, tests and agents use: the buttons call api.setState() and print what api.getState() reads back. Close the dialog with Escape, the backdrop or its button - the named state follows the UI, so the badge flips back to default on its own.
§Discovering the states
Three places name the same list. The skill frontmatter is the machine-readable source - dist/SKILL.md indexes it, and every component's doc page shows the states with a live demo:
---name: Dialogtype: MOL...supportedStates: default, open---## States- `default` - closed (authored initial state)- `open` - shown modally via showModal()```jsdf$('#confirm').get(0).api.setState('open');``` The component scripts also register their states and a registry-level API on globalThis.df$ - useful when you have no element handle yet:
df$.shadcn.dialogStates; // ['default', 'open']// registry form - element passed explicitly, same contract as el.apiconst { dialogApi } = df$.shadcn;const confirm = df$('#confirm').get(0);dialogApi.setState(confirm, 'open');dialogApi.getState(confirm);// → { name: 'open', config: {} }§States across components
Every multi-state component, with its declared state names (from each skill's supportedStates). Components with no .js file are pure HTML/CSS - their only state is default, and their variants are markup attributes (Data Attribute API), not runtime states.
| Component | States |
|---|---|
| Accordion | default · all-open · all-closed |
| Alert Dialog | default · open |
| Animation Canvas | default · overview |
| Avatar | default · error |
| Border Layout | default · collapsed |
| Combobox | default · open |
| Command Palette | default · open |
| Context Menu | default · open |
| Cookie Consent | default · open · preferences · services |
| Countdown | default · running · paused · finished |
| Dialog | default · open |
| Diff | default · before · after |
| Dropdown Menu | default · open |
| File Input | default · dragover · selected · error |
| Image | default · error |
| Menubar | default · open |
| Mermaid | default · rendered · error |
| Navigation Menu | default · open |
| OTP Input | default · filled · invalid |
| Panel | default · minimized · maximized |
| Popover | default · open |
| Presentation | default · notes · fullscreen |
| Product Showcase | default · playing |
| Progress | default · indeterminate · complete |
| Radial Progress | default · indeterminate · complete |
| Search & Filter | default · filled · searching |
| Session | default · detached · streaming |
| Sheet | default · open |
| Sidebar | default · collapsed |
| Slider | default · disabled |
| Table | default · sorted · selected |
| Tabs | default · active · disabled |
| Theme Switcher | default · open |
| Toggle | default · pressed |
| Toggle Group | default · disabled |
| Tooltip | default · visible |
| Tree View | default · expanded |
| Typewriter | default · paused · done |
| Virtual List | default · loading · empty |
| Window | default · maximized · minimized · closed |
§Why it exists
The VAE page describes the proof loop: every declared state of every component must be visually verifiable - a screenshot per state per color scheme, driven through api.setState() by scripts/create-screenshots.ts - and asserted by an e2e test. That is only possible because the states are named, uniform, and callable from plain JavaScript, no matter what the component does internally. Your agents get the same handle: the skills name the states, the API drives them.
See also: How to Use (where the .js layer sits), Data Attribute API (markup configuration, as opposed to runtime state).
Comments, ideas or improvements? Edit this page's source on GitHub