Reactive signals that support both controlled (externally managed) and uncontrolled (internally managed) state — a pattern commonly used in headless UI components.
| Stage | Category | Version | Last Updated | Demo |
|---|---|---|---|---|
| 3 | Reactivity | 1.0.0-next.3 (next) | Aug 12, 2026 | Demo → |
npm i @solid-primitives/controlled-signal@nextReactive signals that support both controlled (externally managed) and uncontrolled (internally managed) state — a pattern commonly used in headless UI components such as dialogs, dropdowns, and toggles.
How it works
A controllable signal has two modes:
- Uncontrolled — when no
valueprop is provided (or it isundefined). The signal manages its own internal state, initialized fromdefaultValue. - Controlled — when a
valueprop is provided. The signal defers entirely to the external value;setValuecallsonChangebut does not update internal state.
This mirrors the controlled/uncontrolled pattern from React, adapted for SolidJS reactivity.
Primitives
createControllableSignal
import { createControllableSignal } from "@solid-primitives/controlled-signal";
const [value, setValue] = createControllableSignal<T>(props);Props:
| Prop | Type | Description |
|---|---|---|
value | Accessor<T | undefined> | Controlled value. When defined, enables controlled mode. |
defaultValue | Accessor<T | undefined> | Initial value for uncontrolled mode. |
onChange | (value: T) => void | Called whenever the value would change. |
Returns: [value: Accessor<T | undefined>, setValue: (next) => void]
Uncontrolled usage
const [open, setOpen] = createControllableSignal({ defaultValue: () => false });
setOpen(true); // updates internal state and calls onChange if providedsetOpen(prev => !prev); // functional formControlled usage
// The consumer drives the state; your component just calls onChange.const [value, setValue] = createControllableSignal({ value: () => props.open, onChange: props.onOpenChange,});
// setValue does NOT update internal state in controlled mode.// It calls onChange so the consumer can update their signal.setValue(true);createControllableBooleanSignal
Variant of createControllableSignal for boolean values. Returns false instead of undefined when unset.
const [open, setOpen] = createControllableBooleanSignal({ defaultValue: () => false,});createControllableArraySignal
Variant for Array<T> values. Returns [] instead of undefined when unset.
const [items, setItems] = createControllableArraySignal<string>({ defaultValue: () => ["a", "b"],});
setItems(prev => [...prev, "c"]);createControllableSetSignal
Variant for Set<T> values. Returns new Set() instead of undefined when unset.
const [selected, setSelected] = createControllableSetSignal<number>({ defaultValue: () => new Set([1, 2]),});
setSelected(prev => new Set([...prev, 3]));createToggleState
Controllable state for toggle components — checkboxes, switches, toggle buttons — built on top of createControllableBooleanSignal. Adapted from Kobalte's createToggleState.
import { createToggleState } from "@solid-primitives/controlled-signal";
const { isSelected, setIsSelected, toggle } = createToggleState({ defaultIsSelected: () => false,});
toggle(); // isSelected() === trueProps:
| Prop | Type | Description |
|---|---|---|
isSelected | Accessor<boolean | undefined> | Controlled selected state. When defined, enables controlled mode. |
defaultIsSelected | Accessor<boolean | undefined> | Initial selected state for uncontrolled mode. |
isDisabled | Accessor<boolean | undefined> | While true, toggle() and setIsSelected() are no-ops. |
isReadOnly | Accessor<boolean | undefined> | While true, toggle() and setIsSelected() are no-ops. |
onSelectedChange | (isSelected: boolean) => void | Called whenever the selected state would change. |
Returns: { isSelected: Accessor<boolean>, setIsSelected: (v: boolean) => void, toggle: () => void }
Just like createControllableSignal, isSelected can be omitted for uncontrolled usage, or provided (alongside onSelectedChange) to let a parent component drive the state.
Credits
This primitive is adapted from the create-controllable-signal and create-toggle-state implementations in Kobalte by the Kobalte Contributors, used under the MIT License.
Changelog
See CHANGELOG.md