useAdjustable

Make a custom value control (a slider, knob or meter) adjustable. It gives the control role="slider" with its value range, arrow/Page/Home/End keys on web, and increment/decrement actions for VoiceOver and TalkBack.
The bar meter is the slider. Tab to it: the arrow keys move by step (Shift+arrow by ten steps, and the horizontal arrows swap under RTL), PageUp/PageDown move by largeStep (default 10% of the range), and Home/End jump to min / max. VoiceOver and TalkBack adjust it with their increment/decrement gestures and read valueText ("60 percent"). useAdjustable({ value, min, max, step, onChange, label, valueText }) returns adjustableProps for the focusable element, increment / decrement (the − and + buttons here) and handleKeyDown; each change is clamped to the range and calls onChange, then onChangeEnd. It also takes disabled, readOnly, orientation: 'vertical', endless (no clamping and no Home/End) and getNextValue for detents.

Loading demo…

import { useState } from 'react'; import { Block, IconButton, useAdjustable } from '@plocks/ui'; const LEVELS = 10; export function Demo() { const [level, setLevel] = useState(6); const { adjustableProps, increment, decrement } = useAdjustable({ value: level, min: 0, max: LEVELS, step: 1, onChange: setLevel, label: 'Volume', valueText: (value) => `${value * 10} percent`, }); return ( <Block direction="row" align="center" gap="sm"> <IconButton icon="minus" variant="ghost" accessibilityLabel="Quieter" onPress={() => decrement()} /> <Block {...adjustableProps} direction="row" align="flex-end" gap={4} p="xs" radius="sm"> {Array.from({ length: LEVELS }, (_, index) => ( <Block key={index} w={8} h={8 + index * 3} radius={2} bg={index < level ? 'primary.5' : 'borderStrong'} /> ))} </Block> <IconButton icon="plus" variant="ghost" accessibilityLabel="Louder" onPress={() => increment()} /> </Block> ); }