Theme
Design your own
On this page (12)

§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 names
el.api.setState('open', { /* optional config */ });
// read it back - always reflects the real UI
el.api.getState(); // → { name: 'open', config: { ... }, model: { ... } }
// the markup of a state - the authored HTML, 1:1, with that state applied
el.api.render();                                  // the current state
el.api.render({ ...el.api.getState(), name: 'default' }); // any declared state

Four 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.

§Reproducible markup

render(state) rebuilds the component's HTML from state: its model is the authored markup byte for byte, and for every state it equals what setState() produces on the live element. Every component with JavaScript has it, and its e2e test proves both.

§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.

§The state is a store

Behind el.api every component keeps its state in a defuss-store store, exposed as el.store - a tiny synchronous observable holding { name, config }. Subscribe to react to any change, including the ones a user makes without setState (a click, Escape, a native close); write it to drive the component exactly like setState does. The big-data components keep their whole query there - a Data Grid's sort, filters, locked columns and selection are el.store.value.config.

§Subscribe and write

The log subscribes to the toggle's store: press the toggle with the mouse or the keyboard - every change arrives, though nothing called setState. The second button writes the store directly; the toggle applies it like setState.

The same primitive is the system's only way into Web Storage: df$.store.persisted(key, initial) returns a store whose value lives in localStorage (or sessionStorage) - validated, versioned, kept in memory when the browser blocks storage, and in step with every other store for that key on the page. Components use it for data-save (border layout, data grid, data tree), the theme switcher and the cookie consent; this site keeps every remembered choice (dark mode, theme, sidebar) that way.

// an observable value
const count = df$.store.create(0);
const off = count.subscribe((n) => console.log('count', n));
count.set(1);                       // → count 1
// a value kept in localStorage - validated against the initial shape,
// in memory when storage is blocked
const prefs = df$.store.persisted('my-app:prefs', { compact: false });
prefs.set('compact', true);         // a path write; stored at once
prefs.subscribe((p) => df$('html').toggleClass('compact', p.compact));
// a component's state, from outside
const grid = df$('#orders').get(0);
grid.store.subscribe(({ config }) => console.log(config.sorters, config.selected));

§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: Dialog
type: MOL
...
supportedStates: default, open
---
## States
- `default` - closed (authored initial state)
- `open` - shown modally via showModal()
```js
df$('#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.api
const { 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.

declared states per JS component
ComponentStates
Accordiondefault · all-open · all-closed
Alert Dialogdefault · open
Animation Canvasdefault · overview
Autocompletedefault · open · loading · empty · error
Avatardefault · error
BibTeXdefault · copied
Border Layoutdefault · collapsed
Comboboxdefault · open
Command Palettedefault · open
Context Menudefault · open
Cookie Consentdefault · open · preferences · services
Countdowndefault · running · paused · finished
Data Griddefault · loading · empty
Data Treedefault · loading · empty
Diagramdefault · playing · paused · active
Dialogdefault · open
Diffdefault · before · after
Document Commentsdefault · current
Dropdown Menudefault · open
Editor.jsdefault · readonly
File Inputdefault · dragover · selected · error
HTML Preview Editordefault · code · state · fullscreen
Iframedefault · loaded
Imagedefault · error
Menubardefault · open
Mermaiddefault · rendered · error
Navigation Menudefault · open
OTP Inputdefault · filled · invalid
Paneldefault · minimized · maximized · closed
Popoverdefault · open
Presentationdefault · notes · fullscreen
Product Showcasedefault · playing
Progressdefault · indeterminate · complete
Property Griddefault · editing
QR Codedefault · empty · error
Questionnairedefault · answering · review · submitted
Radial Progressdefault · indeterminate · complete
Search & Filterdefault · filled · searching
Sessiondefault · detached · streaming
Sheetdefault · open
Sidebardefault · collapsed
Sliderdefault · disabled
Tabledefault · sorted · selected
Tabsdefault · active · disabled
Theme Switcherdefault · open
Toggledefault · pressed
Toggle Groupdefault · disabled
Tooltipdefault · visible
Tree Viewdefault · expanded
Typewriterdefault · paused · done
Virtual Listdefault · loading · empty
Windowdefault · 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: Anatomy of a Component (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