Controlled State
Use controlled state with the current published Askr packages.
@askrjs/ui0.0.13@askrjs/themes0.0.13
How to use controlled state
Controlled State should use the standard themed surface unless the application is deliberately implementing its own design system on top of @askrjs/ui.
- Import the themed component from @askrjs/themes/components; use @askrjs/ui directly only when building a custom visual system.
- Keep controlled state labels, state, and application actions visible at the call site instead of styling internal DOM nodes.
- Verify keyboard behavior, focus movement or return, disabled state, and both standard themes.
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@askrjs/themes/components';
<Select value={status()} onValueChange={status.set}>
<SelectTrigger aria-label="Project status"><SelectValue /></SelectTrigger>
<SelectContent>
{statuses.map((item) => <SelectItem value={item.id}>{item.label}</SelectItem>)}
</SelectContent>
</Select>Purpose
Nearly every stateful component in @askrjs/ui — Checkbox, Switch, RadioGroup, Select, Accordion, ToggleGroup, Dialog, Popover, Dropdown, Menubar — follows the same value/change-handler naming convention, so once you've controlled one you've effectively controlled all of them. This page documents that shared convention rather than repeating it on every component page.
Install and import
There's no separate import for this — it's a prop-naming pattern baked into each component's own types, e.g. `CheckboxOwnProps`, `AccordionSingleProps`, `SelectOwnProps`. You use it just by passing the relevant props when you import the component itself.
Live examples
`<Checkbox checked={checked} onCheckedChange={setChecked} />` is fully controlled — the component renders exactly what `checked` says and calls back on every toggle. Drop `checked`/`onCheckedChange` and pass `defaultChecked` instead, and the same component manages its own internal state, useful for a form where you don't need to observe every change.
Anatomy
The pattern has three parts per value it tracks: a controlled prop (`value`, `checked`, `open`), an uncontrolled seed (`defaultValue`, `defaultChecked`, `defaultOpen`), and a change callback (`onValueChange`, `onCheckedChange`, `onOpenChange`). Select is a good example of a component tracking two independent pieces of state this way at once — `value`/`defaultValue`/`onValueChange` for the selected option and `open`/`defaultOpen`/`onOpenChange` for the popover's visibility.
State model
Passing the controlled prop (even `undefined` isn't the trigger — actually passing the named prop key is) puts the component in controlled mode for that value; omitting it and optionally passing the `default*` variant puts it in uncontrolled mode. Checkbox's docs note explicitly that `indeterminate` is visual-only and independent of `checked` — setting it doesn't imply or require a particular checked value, so you manage it as its own boolean regardless of controlled/uncontrolled status.
Keyboard and accessibility
Controlled or uncontrolled makes no difference to keyboard behavior — Space still toggles Checkbox and Switch, arrow keys still move focus in RadioGroup and ToggleGroup — because that logic lives in the component's internal interaction handling, not in whichever half of the value it happens to own at a given moment. The distinction only matters for how the resulting value change is surfaced to your code.
Styling and tokens
Whichever mode you're in, the rendered `data-state` (e.g. `data-state="checked"` or `data-state="open"`) reflects the resolved current value, so themed CSS never needs to know or care whether that value came from your state or the component's internal state.
API
Representative shapes across the library: Checkbox/Switch use `checked?: boolean`, `defaultChecked?: boolean`, `onCheckedChange?: (checked: boolean) => void`; RadioGroup/ToggleGroup/Select use `value?: string`, `defaultValue?: string`, `onValueChange?: (value: string) => void` (or `string[]` for multi-select ToggleGroup and multi-mode Accordion); overlay triggers use `open?: boolean`, `defaultOpen?: boolean`, `onOpenChange?: (open: boolean) => void`.
Edge cases
Switching a component between controlled and uncontrolled across renders (e.g. `checked={isEditing ? checked : undefined}`) isn't a pattern any of these components are designed to support cleanly — pick one mode when the component mounts and stick with it, matching the same rule React itself applies to controlled inputs. If you pass both `value` and `defaultValue`, the controlled `value` wins and `defaultValue` is simply ignored, since it only seeds initial state in uncontrolled mode.
Related pages
See Collections for how this convention extends to multi-item selection state. See the individual component pages (Checkbox, Switch, Select, Accordion) for the exact prop names each one uses.