@solid-primitives/controlled-signal

Reactive 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.
Installation
npm install @solid-primitives/controlled-signal
yarn add @solid-primitives/controlled-signal
pnpm add @solid-primitives/controlled-signal
How it works
A controllable signal has two modes:
- Uncontrolled — when no
value prop is provided (or it is undefined). The signal manages its own internal state, initialized from defaultValue.
- Controlled — when a
value prop is provided. The signal defers entirely to the external value; setValue calls onChange but 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:
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);
setOpen(prev => !prev);
Controlled usage
const [value, setValue] = createControllableSignal({
value: () => props.open,
onChange: props.onOpenChange,
});
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();
Props:
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