Headless hooks
valyro-ui/headless: the behaviour hooks the components are built on — focus, keyboard, floating layers, dismiss — without markup or CSS, with live examples.
import { useDisclosure, useDismissableLayer, useFloating } from 'valyro-ui/headless';
State
useControllableState
useControllableState({ value, defaultValue, onChange }) → [value, setValue]
A value that works controlled (value + onChange) or uncontrolled (defaultValue); setValue takes a value or an updater.
useDisclosure
useDisclosure({ open?, defaultOpen?, onOpenChange? }) → { open, setOpen, onOpen, onClose, onToggle }
Open / closed state of a dialog, drawer, popover or menu, controlled or not.
Keyboard and focus
useListNavigation
useListNavigation({ count, isDisabled?, getLabel?, locale?, onSelect?, loop?, orientation?, initialIndex? }) → { activeIndex, setActiveIndex, onKeyDown }
Menu and listbox keys: arrows, Home / End, Enter / Space and type-ahead (with getLabel, compared with the locale collator: "e" finds "Éclair"). Focus stays on the container; mark the active row with aria-activedescendant.
stepIndex
stepIndex(count, from, delta, isDisabled?, loop?) → index
The next enabled index in a direction, skipping disabled ones.
useFocusTrap
useFocusTrap(ref, active, { initialFocusRef?, restoreFocus?, returnFocusRef?, contain? })
Keeps Tab inside ref while active, focuses the first tabbable (or initialFocusRef) and gives focus back on release; if that element is gone, to returnFocusRef or the nearest place it was. contain (modals) also brings back focus that lands outside by other means (a screen reader, a click). Traps stack.
getTabbables
getTabbables(container) → HTMLElement[]
The elements Tab visits inside a container, in order.
useDismissableLayer
useDismissableLayer({ enabled, refs, onDismiss, closeOnEscape?, closeOnOutside?, passThrough? })
Escape and outside presses for a layer. Open layers form one stack: Escape closes the topmost, an outside press the ones it lands outside of.
DISMISS_IGNORE_ATTRIBUTE
'data-ui-dismiss-ignore'
Presses inside an element with this attribute dismiss no layer (toasts, floating toolbars).
useScrollLock
useScrollLock(active)
Locks page scroll while active, without a layout shift; ref-counted for nested modals.
ariaHideOutside
ariaHideOutside(targets, root = document.body) → undo
Hides everything outside targets from screen readers with aria-hidden, as React Aria does for modals: aria-modal alone does not stop every virtual cursor. Keeps the live announcer, toasts and layers opened meanwhile readable; nests.
useAriaHideOutside
useAriaHideOutside(refs, active)
ariaHideOutside for the elements in refs while active.
useTypeahead
useTypeahead(locale?, timeout?) → { type(event), isTyping(), matches(label, query) }
The type-ahead buffer of one widget: keys typed in a row build one query ("new y" → "New York"); while typing, Space belongs to the query.
createPrefixMatcher
createPrefixMatcher(locale?) → (label, query) => boolean
Whether a label starts with a query, ignoring case and accents for the locale ("e" finds "Éclair").
createContainsMatcher
createContainsMatcher(locale?) → (text, query) => boolean
Whether a text contains a query anywhere, ignoring case and accents for the locale ("resume" finds "Résumé"): the filter of the comboboxes and command palette.
findTypeaheadIndex
findTypeaheadIndex(count, query, activeIndex, getLabel, matches, isDisabled?) → index | -1
The item a query selects: a repeated letter cycles through the items starting with it, a longer query refines the match.
Screen-reader announcements
announce
announce(message, assertiveness = 'assertive', timeout?)
Reads a change that has no visible text of its own (a new sort order, a drop target, a result count) through one hidden live region; plain DOM, callable from any handler.
clearAnnouncer
clearAnnouncer(assertiveness?)
Drops pending messages, e.g. when what they describe is gone.
destroyAnnouncer
destroyAnnouncer()
Removes the live region from the page (tests, unmounting micro-frontends); the next announce creates it again.
LIVE_ANNOUNCER_ATTRIBUTE
'data-live-announcer'
On the announcer root, so layers that hide the page from screen readers can leave it readable.
useAnnounceCount
useAnnounceCount(count, { active, message, key?, delay? })
Reads how many results a filtered list holds once typing pauses ("5 results available"), as React Aria's combobox does; skips zero, which the empty state says itself.
Layers and positioning
useFloating
useFloating({ open, anchorRef, floatingRef, placement?, offset?, padding?, matchAnchorWidth? }) → { floatingStyles, side, align, update }
Places a position: fixed panel next to an anchor (an element or a virtual one with getBoundingClientRect), flips and shifts it to stay on screen, follows scroll and resize. Sets --ui-available-height and the arrow offsets.
computePosition
computePosition({ anchor, floating, viewport, placement, offset, padding, rtl? }) → { x, y, side, align, … }
The pure geometry behind useFloating, for canvases and tests.
parsePlacement
parsePlacement('bottom-start') → ['bottom', 'start']
Splits a placement into side and alignment.
usePresence
usePresence(open, exitMs?) → { isMounted, state: 'open' | 'closed' }
Keeps an element mounted while its exit animation plays; animate [data-state="closed"].
Portal
<Portal container?>
Renders into the nearest ThemeScope’s layer container (so the layer is themed like its page), else document.body; nothing on the server.
useIsClient
useIsClient() → boolean
False during SSR and hydration, true after: client-only markup without hydration mismatches.
usePortalContainer
usePortalContainer() → Element | null
The element Portal renders into.
Slot
<Slot {...props}>{child}</Slot>
Merges its props into its single child: the asChild pattern. Handlers both run (the child’s first; yours is skipped after its preventDefault()), classes join, styles and refs merge.
mergeProps
mergeProps(slotProps, childProps) → props
The merge Slot does, for your own components.
Lists and motion
useVirtualizer
useVirtualizer({ count, scrollRef, estimateSize, getItemKey?, horizontal?, overscan?, gap?, … }) → { items, totalSize, range, scrollToIndex, measureElement, … }
Renders only the visible rows of a long list: fixed or measured sizes, horizontal lists, scroll-to-index, scroll anchoring on prepend, SSR.
useFlip
useFlip(containerRef, { spring?, enabled?, animateEnter?, auto? }) → capture
FLIP layout animation: call capture() before a change; elements with data-flip-id slide to their new place on a spring. Off under reduced motion.
flipDelta
flipDelta(first, last) → { x, y, scaleX, scaleY } | null
How far a box moved between two measurements.
useSpringValue
useSpringValue(target, onFrame, { spring?, tempoRef?, enabled? })
Springs a number towards target and writes each frame through onFrame, without a React render per frame.
useNow
useNow(intervalMs?) → number
The current time, re-rendering every intervalMs from one timer shared by every caller of the interval.
subscribeNow
subscribeNow(intervalMs, listener) → unsubscribe
The same shared clock outside React.
getNow
getNow(intervalMs) → number
The time of that clock, stable within a render pass.
Components without markup
useAccordion
useAccordion({ type?, collapsible?, value?, defaultValue?, onValueChange? }) → { value, isOpen, toggle, getRootProps, getItemProps, getTriggerProps, getPanelProps }
The Accordion kit component renders with this hook: open sections (single or multiple), trigger ↔ panel ids, arrows / Home / End between triggers. Each getter takes the section and your props and merges them (your handler first).
useTreeView
useTreeView({ nodes, expanded?, defaultExpanded?, selected?, defaultSelected?, … }) → { visible, isExpanded, setExpanded, activate, focus, getTreeProps, getItemProps, getRowProps, getGroupProps }
The TreeView kit component renders with this hook: expanded and selected nodes, a roving tab stop and the ARIA tree keys (arrows, also under RTL, Home / End, Enter / Space, type-ahead).
useDataTable
useDataTable({ data, getRowId?, getValue?, getSearchText?, sort?, selected?, query?, pageSize?, page?, … }) → { rows, results, toggleSort, setQuery, setPage, toggleRow, toggleAll, getColumnProps, getSortProps, getRowProps, getRowCheckboxProps, getSelectAllProps, getSearchProps }
The row model of DataTable: search, a sort that cycles ascending → descending → off, pages and row selection, each controlled or not. Render it as a table, a list or a grid of cards.
useListbox
useListbox({ items, multiple?, value?, defaultValue?, onValueChange?, focusable?, selectionBehavior?, … }) → { options, value, activeIndex, select, selectRange, selectAll, onKeyDown, getListboxProps, getGroupProps, getGroupLabelProps, getOptionProps }
The option list of Listbox, Select, MultiSelect and Combobox: options flat or in groups, one or several selected, the highlighted option and the listbox keys. Focusable on its own, or driven by a trigger or an input (focusable: false). A focusable multiple list selects ranges as React Aria does: Shift+click, Shift+arrows / Home / End / Page keys, Shift+Space, Ctrl/Cmd+A, Escape to clear; selectionBehavior: 'replace' selects like a file list.
useSelect
useSelect({ items, multiple?, value?, open?, searchable?, floating?, native?, … }) → { value, selected, open, show, close, listbox, getTriggerProps, getPopupProps, getSearchProps, getListboxProps, getOptionProps, getNativeSelectProps }
Select and MultiSelect without markup: a button trigger (select-only combobox) that opens a listbox, letters jump to an option, Alt+↑ picks and closes, an optional filter box. With floating it positions the popup and closes it on an outside press or Escape. With native the field is a <select> (getNativeSelectProps): the phone's own picker, form validation and autofill.
useCombobox
useCombobox({ items, value?, inputValue?, filter?, allowCustomValue?, selectionMode?, onSelect?, … }) → { value, inputValue, open, search, listbox, getInputProps, getFieldProps, getToggleButtonProps, getPopupProps, getListboxProps, getOptionProps }
Combobox without markup: typing filters and opens the list, the highlight follows its option when results change, leaving the field resolves the text. selectionMode: "none" turns it into a command menu: CommandPalette renders with it.
useRadioGroup
useRadioGroup({ items, value?, defaultValue?, onValueChange?, disabled?, required? }) → { value, getGroupProps, getInputProps, getRadioProps, getDescriptionId }
RadioGroup and SegmentedControl without markup: native radios (getInputProps) or any element as a radio (getRadioProps) with a roving tab stop, every arrow key and selection on focus.
useCheckboxGroup
useCheckboxGroup({ items, value?, defaultValue?, onValueChange?, disabled?, readOnly?, required?, invalid?, name? }) → { value, isChecked, toggle, getGroupProps, getInputProps, getDescriptionId }
CheckboxGroup without markup: the checked values of native checkboxes in a group. required means at least one: while none is checked every box is required, so the form does not submit; invalid, disabled and readOnly reach every box.
useField
useField({ id?, label?, description?, invalid?, required?, counter? }) → { id, context, getLabelProps, getControlProps, getDescriptionProps, getErrorProps, getCounterProps }
The wiring of FormField: one id per part, the label pointing at the control, its aria-describedby (description or error, counter), aria-invalid and aria-required. context is what useFormField reads inside a FormField.
useTabs
useTabs({ tabs, value?, defaultValue?, onValueChange?, orientation?, activation? }) → { value, getListProps, getTabProps, getPanelProps }
Tabs without markup: tab ↔ panel ids, a roving tab stop, ←/→ (mirrored under RTL) or ↑/↓ when vertical, Home / End, and automatic or manual activation.
useMenu
useMenu({ items, open?, onClose?, onBack?, defaultHighlight?, floating?, … }) → { open, show, close, entries, activeIndex, submenu, getTriggerProps, getTargetProps, getMenuProps, getItemProps, getSubmenuOptions }
One menu level shared by DropdownMenu and ContextMenu: actions, checkboxes and radio rows, type-ahead, submenus (another useMenu with getSubmenuOptions), a menu button or a context target (right-click, long-press, Shift+F10).
usePagination
usePagination({ pageCount, value?, defaultValue?, onValueChange?, siblings?, boundaries? }) → { page, range, setPage, getListProps, getPreviousProps, getNextProps, getPageProps }
Pagination without markup: the page range with ellipses (getPaginationRange, a pure function), previous / next, arrow keys between the buttons, focus kept when a step button is disabled.
getPaginationRange
getPaginationRange({ page, pageCount, siblings?, boundaries? }) → (number | "start-ellipsis" | "end-ellipsis")[]
The page numbers and ellipses to show; constant length once collapsing starts, so the control never changes width.
useDialog
useDialog({ open?, defaultOpen?, onOpenChange?, role?, modal?, layer? }) → { open, setOpen, titleId, descriptionId, getTriggerProps, getCloseProps, getDialogProps, getTitleProps, getDescriptionProps }
Dialog and Drawer without markup: open state, the title and description ids that name the dialog, the dialog-button ARIA, and with layer the modal layer itself: focus trap, scroll lock, Escape and outside press.
useTooltip
useTooltip({ open?, defaultOpen?, onOpenChange?, delay?, disabled?, floating?, placement? }) → { visible, show, hide, getTriggerProps, getTooltipProps }
Tooltip without markup: hover opens after a delay and keyboard focus at once, a grace period to reach the tooltip (WCAG 1.4.13), Escape, one tooltip at a time and a shared warm-up, aria-describedby on the trigger.
useResizablePanels
useResizablePanels({ panels, direction?, layout?, onLayout?, autoSaveId?, keyboardStep? }) → { layout, collapse, expand, resize, getGroupProps, getPanelProps, getHandleProps }
ResizablePanels without markup: sizes in percent with pixel limits, collapsing, a saved layout, and the window-splitter handle (drag, arrows, Home / End, Enter, double click).
useDateField
useDateField({ value?, defaultValue?, onValueChange?, locale?, minDate?, maxDate?, … }) → { value, setValue, outOfRange, segments, getGroupProps, focusFirst }
The DateField kit component renders with this hook: day, month and year as spinbutton segments in the locale's order (Intl formatToParts), typed digit by digit or stepped with the arrows, as React Aria's DateField. Render each of segments as a span with its props.
useTimeField
useTimeField({ value?, defaultValue?, onValueChange?, hourCycle, locale?, … }) → { value, setValue, segments, getGroupProps, focusFirst }
The same for a time: hour, minute and, on a 12-hour clock, AM/PM; the value is 24-hour "HH:mm".
useCheckboxTree
useCheckboxTree({ nodes, value?, defaultValue?, onValueChange?, expanded?, defaultExpanded?, … }) → { value, visible, getCheckState, toggle, isExpanded, setExpanded, focus, getTreeProps, getItemProps, getRowProps, getToggleProps, getGroupProps }
The CheckboxTree kit component renders with this hook: checked leaves, branches checked, mixed or unchecked from them, a branch that toggles its enabled leaves, expanded branches, a roving tab stop and the tree keys (arrows, also under RTL, Home / End, Space / Enter).
useStepper
useStepper({ steps, value?, defaultValue?, onValueChange?, clickable? }) → { value, setValue, getStepState, isClickable, getStepProps, getTriggerProps }
Stepper without markup: each step complete, current, upcoming or failed, aria-current="step", and with clickable the passed steps as buttons that go back; the step gone back to keeps the focus.
useCalendar
useCalendar({ mode?, value?, defaultValue?, onValueChange?, month?, numberOfMonths?, locale?, weekStartsOn?, minDate?, maxDate?, … }) → { value, months, weekdays, getWeeks, getDayState, select, shiftMonth, getRootProps, getPreviousProps, getNextProps, getCaptionProps, getGridProps, getWeekdayProps, getCellProps, getDayProps }
The Calendar kit component renders with this hook: one date or a range (the end previewed under the pointer, both picks read out), months side by side, the week from the locale's first day, min / max / disabled days and the date-grid keys (arrows, PageUp / PageDown, Home / End). A new month sets data-turn on the root for a CSS page turn.
useSlider
useSlider({ value?, defaultValue?, onValueChange?, min?, max?, step?, disabled?, formatValue?, name?, … }) → { values, isRange, dragging, setThumbValue, getRootProps, getTrackProps, getRangeProps, getThumbProps, getInputProps, getThumbLabelProps, getHiddenInputProps }
Slider without markup: one value or a range whose thumbs cannot pass each other, a press that grabs the nearest thumb, the keys (arrows mirrored under RTL, PageUp / PageDown, Home / End) and a visually hidden native range input per thumb, so VoiceOver and TalkBack swipes change it.
useSortable
useSortable({ items?, defaultItems?, onReorder?, getKey?, layout?, handle?, isItemDisabled?, getItemLabel?, labels? }) → { shown, grabbed, overlayItem, lift, drop, cancel, move, getMoveActions, getListProps, getItemProps, getHandleProps, getStartButtonProps, getOverlayProps, getInstructionsProps }
Sortable without markup: a pointer drag with a copy that follows the pointer (press-and-hold on touch), Space / arrows / Enter / Escape from the keyboard, a start button and move buttons for screen readers and a single pointer (WCAG 2.5.7), every step read out, and items gliding to their new place.
useCarousel
useCarousel({ count, slidesPerView?, gap?, index?, defaultIndex?, onIndexChange?, loop?, autoplay?, align? }) → { index, pages, goTo, previous, next, rotating, togglePlay, getRootProps, getTrackProps, getSlideProps, getPreviousProps, getNextProps, getRotationProps, getIndicatorsProps, getIndicatorProps, getLiveRegionProps }
Carousel without markup (APG carousel): a scroll-snap track whose page follows swipes and scrolling, previous / next, keys on the track, a mouse drag with a flick, dots or thumbnails, autoplay that pauses on hover, focus and reduced motion, and a polite live region.
usePopover
usePopover({ open?, defaultOpen?, onOpenChange?, floating?, placement?, offset? }) → { open, setOpen, triggerRef, contentRef, getTriggerProps, getContentProps }
Popover without markup: a trigger that toggles a non-modal dialog named by the trigger unless it has a name of its own, focus moved in on open and back on close, Tab past the panel continuing from the trigger's place in the page. With floating it positions the panel and closes it on an outside press or Escape.
useNumberInput
useNumberInput({ value?, defaultValue?, onValueChange?, min?, max?, step?, precision?, locale?, formatOptions?, allowWheel?, … }) → { value, inputValue, increment, decrement, getInputProps, getIncrementProps, getDecrementProps, getHiddenInputProps }
NumberInput without markup: a spinbutton in the locale format, typed text kept as a draft and committed on blur or Enter (clamped and rounded), ↑ / ↓ (Shift ×10), PageUp / PageDown, Home / End, the wheel while focused, and step buttons that repeat faster while held.
Refs, events and direction
mergeRefs
mergeRefs(...refs) → ref callback
One ref for several: object refs and callbacks, cleanups included.
composeHandlers
composeHandlers(theirs, ours) → handler
Calls the user’s handler first and skips yours when it called preventDefault().
useLatest
useLatest(value) → ref
A ref that always holds the latest value, for effects and stable callbacks.
useEventCallback
useEventCallback(fn) → fn
A stable function that always calls the latest implementation.
cx
cx('a', cond && 'b', undefined) → 'a b'
Joins class names, skipping falsy values.
useDirection
useDirection(ref) → 'ltr' | 'rtl'
The writing direction at an element, re-read after each render.
getDirection
getDirection(element) → 'ltr' | 'rtl'
The same, read once from CSS (follows dir on any ancestor).
inlineArrowStep
inlineArrowStep(key, direction) → -1 | 0 | 1
ArrowLeft / ArrowRight as a step along the inline axis, mirrored in RTL.
isComposingKey
isComposingKey(event) → boolean
True while an IME composes (Japanese, Chinese, Korean input): leave Enter and arrows to it.
clamp
clamp(value, min, max) → number
Keeps a number within a range.
React 17–19 helpers
useId
useId() → string
React’s useId on 18+; on React 17 an id assigned right after mount (none in server HTML, so hydration matches).
useSyncExternalStore
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
React’s on 18+, an equivalent from state and effects on 17.
useIsomorphicLayoutEffect
useIsomorphicLayoutEffect(effect, deps)
useLayoutEffect in the browser, useEffect on the server (no SSR warning).
withRef
withRef(render, statics?)
ref as a prop on every version: the render itself on React 19, forwardRef before. statics become compound parts (Card.Header).
cleanupRef
cleanupRef(node => cleanup) → ref callback
A ref callback that returns a cleanup, the React 19 way, also on 17 and 18.
sharedCleanupRef
sharedCleanupRef(node => cleanup) → ref callback
cleanupRef for one stable callback many elements share (ref={measure} on every row).
getElementRef
getElementRef(element) → ref
The ref of an element: props.ref on React 19, element.ref before.
startTransition
startTransition(scope)
React’s on 18+; runs the update at once on 17.
useTransition
useTransition() → [isPending, startTransition]
React’s on 18+; never pending on 17.
useDeferredValue
useDeferredValue(value) → value
React’s on 18+; the value itself on 17.