Headless-хуки
valyro-ui/headless: хуки поведения, на которых построены компоненты, — фокус, клавиатура, всплывающие слои, закрытие — без разметки и CSS, с живыми примерами.
import { useDisclosure, useDismissableLayer, useFloating } from 'valyro-ui/headless';
Состояние
useControllableState
useControllableState({ value, defaultValue, onChange }) → [value, setValue]
Значение, которое работает и контролируемо (value + onChange), и нет (defaultValue); setValue принимает значение или функцию.
useDisclosure
useDisclosure({ open?, defaultOpen?, onOpenChange? }) → { open, setOpen, onOpen, onClose, onToggle }
Открыт или закрыт диалог, шторка, поповер или меню — контролируемо или нет.
Клавиатура и фокус
useListNavigation
useListNavigation({ count, isDisabled?, getLabel?, locale?, onSelect?, loop?, orientation?, initialIndex? }) → { activeIndex, setActiveIndex, onKeyDown }
Клавиши меню и списков: стрелки, Home / End, Enter / Space и поиск по первым буквам (с getLabel, сравнение через Collator локали locale: «e» находит «Éclair»). Фокус остаётся на контейнере, активную строку отмечает aria-activedescendant.
stepIndex
stepIndex(count, from, delta, isDisabled?, loop?) → index
Следующий доступный индекс в направлении, недоступные пропускаются.
useFocusTrap
useFocusTrap(ref, active, { initialFocusRef?, restoreFocus?, returnFocusRef?, contain? })
Пока активен, держит Tab внутри ref, фокусирует первый доступный элемент (или initialFocusRef) и возвращает фокус при выходе; если того элемента уже нет — на returnFocusRef или ближе всего к месту, где он был. contain (модальные окна) возвращает и фокус, попавший наружу иначе (экранный диктор, щелчок). Ловушки складываются в стек.
getTabbables
getTabbables(container) → HTMLElement[]
Элементы, по которым ходит Tab внутри контейнера, по порядку.
useDismissableLayer
useDismissableLayer({ enabled, refs, onDismiss, closeOnEscape?, closeOnOutside?, passThrough? })
Escape и нажатие снаружи для слоя. Открытые слои образуют один стек: Escape закрывает верхний, нажатие снаружи — те, вне которых оно пришлось.
DISMISS_IGNORE_ATTRIBUTE
'data-ui-dismiss-ignore'
Нажатия внутри элемента с этим атрибутом не закрывают слои (тосты, плавающие панели).
useScrollLock
useScrollLock(active)
Блокирует прокрутку страницы без сдвига вёрстки; со счётчиком для вложенных модальных окон.
ariaHideOutside
ariaHideOutside(targets, root = document.body) → undo
Скрывает от экранных дикторов всё вне targets через aria-hidden, как React Aria для модальных окон: одного aria-modal хватает не всем виртуальным курсорам. Оставляет доступными объявления, тосты и слои, открытые поверх; вложенные вызовы складываются.
useAriaHideOutside
useAriaHideOutside(refs, active)
ariaHideOutside для элементов из refs, пока active.
useTypeahead
useTypeahead(locale?, timeout?) → { type(event), isTyping(), matches(label, query) }
Буфер поиска по первым буквам для одного виджета: клавиши подряд складываются в один запрос («new y» → «New York»); пока идёт ввод, пробел относится к запросу.
createPrefixMatcher
createPrefixMatcher(locale?) → (label, query) => boolean
Начинается ли подпись с запроса — без учёта регистра и диакритики для локали («e» находит «Éclair»).
createContainsMatcher
createContainsMatcher(locale?) → (text, query) => boolean
Содержит ли текст запрос в любом месте — без учёта регистра и диакритики для локали («resume» находит «Résumé»): фильтр комбобоксов и палитры команд кита.
findTypeaheadIndex
findTypeaheadIndex(count, query, activeIndex, getLabel, matches, isDisabled?) → index | -1
Элемент, который выбирает запрос: повтор одной буквы перебирает элементы на неё, более длинный запрос уточняет совпадение.
Объявления для экранных дикторов
announce
announce(message, assertiveness = 'assertive', timeout?)
Зачитывает изменение без собственного видимого текста (новый порядок сортировки, место сброса, число результатов) через одну скрытую live-область; обычный DOM, вызывается из любого обработчика.
clearAnnouncer
clearAnnouncer(assertiveness?)
Убирает ещё не прочитанные сообщения, например когда то, о чём они, исчезло.
destroyAnnouncer
destroyAnnouncer()
Удаляет live-область со страницы (тесты, размонтирование микрофронтендов); следующий announce создаст её заново.
LIVE_ANNOUNCER_ATTRIBUTE
'data-live-announcer'
Стоит на корне объявлений, чтобы слои, скрывающие страницу от дикторов, оставляли его доступным.
useAnnounceCount
useAnnounceCount(count, { active, message, key?, delay? })
Зачитывает, сколько результатов в отфильтрованном списке, когда ввод затих («5 результатов»), как комбобокс React Aria; ноль пропускает — его сообщает пустое состояние.
Слои и позиционирование
useFloating
useFloating({ open, anchorRef, floatingRef, placement?, offset?, padding?, matchAnchorWidth? }) → { floatingStyles, side, align, update }
Ставит панель с position: fixed рядом с якорем (элемент или виртуальный объект с getBoundingClientRect), переворачивает и сдвигает её, чтобы она не уходила за экран, следит за прокруткой и размером. Задаёт --ui-available-height и смещение стрелки.
computePosition
computePosition({ anchor, floating, viewport, placement, offset, padding, rtl? }) → { x, y, side, align, … }
Чистая геометрия useFloating — для canvas и тестов.
parsePlacement
parsePlacement('bottom-start') → ['bottom', 'start']
Делит placement на сторону и выравнивание.
usePresence
usePresence(open, exitMs?) → { isMounted, state: 'open' | 'closed' }
Не размонтирует элемент, пока идёт анимация закрытия; анимируйте [data-state="closed"].
Portal
<Portal container?>
Рендерит в контейнер слоёв ближайшего ThemeScope (слой получает тему своей страницы), иначе в document.body; на сервере — ничего.
useIsClient
useIsClient() → boolean
false при SSR и гидрации, потом true: разметка только для клиента без расхождений гидрации.
usePortalContainer
usePortalContainer() → Element | null
Элемент, в который рендерит Portal.
Slot
<Slot {...props}>{child}</Slot>
Передаёт свои пропсы единственному ребёнку — паттерн asChild. Обработчики вызываются оба (сначала ребёнка; ваш пропускается после его preventDefault()), классы объединяются, стили и ref сливаются.
mergeProps
mergeProps(slotProps, childProps) → props
То же слияние, что делает Slot, — для своих компонентов.
Списки и движение
useVirtualizer
useVirtualizer({ count, scrollRef, estimateSize, getItemKey?, horizontal?, overscan?, gap?, … }) → { items, totalSize, range, scrollToIndex, measureElement, … }
Рендерит только видимые строки длинного списка: фиксированный или измеренный размер, горизонтальные списки, прокрутка к индексу, якорь при добавлении сверху, SSR.
useFlip
useFlip(containerRef, { spring?, enabled?, animateEnter?, auto? }) → capture
FLIP-анимация раскладки: вызовите capture() перед изменением — элементы с data-flip-id переедут на новое место на пружине. Выключена при reduced motion.
flipDelta
flipDelta(first, last) → { x, y, scaleX, scaleY } | null
На сколько сдвинулся прямоугольник между двумя замерами.
useSpringValue
useSpringValue(target, onFrame, { spring?, tempoRef?, enabled? })
Ведёт число к target на пружине и отдаёт каждый кадр в onFrame — без рендера React на каждый кадр.
useNow
useNow(intervalMs?) → number
Текущее время с перерисовкой каждые intervalMs от одного таймера на всех, кто подписан на этот интервал.
subscribeNow
subscribeNow(intervalMs, listener) → unsubscribe
Те же общие часы вне React.
getNow
getNow(intervalMs) → number
Время этих часов, стабильное в пределах одного рендера.
Компоненты без разметки
useAccordion
useAccordion({ type?, collapsible?, value?, defaultValue?, onValueChange? }) → { value, isOpen, toggle, getRootProps, getItemProps, getTriggerProps, getPanelProps }
На этом хуке построен Accordion кита: открытые секции (одна или несколько), связь id кнопки и панели, стрелки / Home / End между кнопками. Каждый геттер принимает секцию и ваши пропсы и сливает их (ваш обработчик первым).
useTreeView
useTreeView({ nodes, expanded?, defaultExpanded?, selected?, defaultSelected?, … }) → { visible, isExpanded, setExpanded, activate, focus, getTreeProps, getItemProps, getRowProps, getGroupProps }
На этом хуке построен TreeView кита: раскрытые и выбранный узлы, роуминговый tabindex и клавиши дерева ARIA (стрелки, в том числе в RTL, Home / End, Enter / Space, поиск по буквам).
useDataTable
useDataTable({ data, getRowId?, getValue?, getSearchText?, sort?, selected?, query?, pageSize?, page?, … }) → { rows, results, toggleSort, setQuery, setPage, toggleRow, toggleAll, getColumnProps, getSortProps, getRowProps, getRowCheckboxProps, getSelectAllProps, getSearchProps }
Модель строк DataTable: поиск, сортировка по кругу «по возрастанию → по убыванию → выкл.», страницы и выбор строк, всё контролируемо или нет. Выводите как таблицу, список или сетку карточек.
useListbox
useListbox({ items, multiple?, value?, defaultValue?, onValueChange?, focusable?, selectionBehavior?, … }) → { options, value, activeIndex, select, selectRange, selectAll, onKeyDown, getListboxProps, getGroupProps, getGroupLabelProps, getOptionProps }
Список вариантов Listbox, Select, MultiSelect и Combobox: варианты плоско или группами, один или несколько выбранных, подсвеченный вариант и клавиши listbox. Фокусируется сам или управляется кнопкой либо полем (focusable: false). Фокусируемый список с multiple выбирает диапазоны, как React Aria: Shift+клик, Shift+стрелки / Home / End / Page, Shift+Space, Ctrl/Cmd+A, Escape снимает выбор; selectionBehavior: 'replace' — выбор как в списке файлов.
useSelect
useSelect({ items, multiple?, value?, open?, searchable?, floating?, native?, … }) → { value, selected, open, show, close, listbox, getTriggerProps, getPopupProps, getSearchProps, getListboxProps, getOptionProps, getNativeSelectProps }
Select и MultiSelect без разметки: кнопка-триггер (select-only combobox) открывает listbox, буквы прыгают к варианту, Alt+↑ выбирает и закрывает, по желанию — поле фильтра. С floating хук позиционирует попап и закрывает его по клику снаружи и Escape. С native поле — нативный <select> (getNativeSelectProps): системный выбор на телефоне, проверка формы и автозаполнение.
useCombobox
useCombobox({ items, value?, inputValue?, filter?, allowCustomValue?, selectionMode?, onSelect?, … }) → { value, inputValue, open, search, listbox, getInputProps, getFieldProps, getToggleButtonProps, getPopupProps, getListboxProps, getOptionProps }
Combobox без разметки: ввод фильтрует и открывает список, подсветка остаётся на своём варианте, когда результаты меняются, уход с поля превращает текст в значение. selectionMode: "none" делает из него меню команд: на нём построен CommandPalette.
useRadioGroup
useRadioGroup({ items, value?, defaultValue?, onValueChange?, disabled?, required? }) → { value, getGroupProps, getInputProps, getRadioProps, getDescriptionId }
RadioGroup и SegmentedControl без разметки: нативные radio (getInputProps) или любой элемент как radio (getRadioProps) с роуминговым tabindex, всеми стрелками и выбором при фокусе.
useCheckboxGroup
useCheckboxGroup({ items, value?, defaultValue?, onValueChange?, disabled?, readOnly?, required?, invalid?, name? }) → { value, isChecked, toggle, getGroupProps, getInputProps, getDescriptionId }
CheckboxGroup без разметки: отмеченные значения нативных флажков в group. required — хотя бы один: пока ни один не отмечен, каждый флажок required, и форма не отправится; invalid, disabled и readOnly доходят до каждого флажка.
useField
useField({ id?, label?, description?, invalid?, required?, counter? }) → { id, context, getLabelProps, getControlProps, getDescriptionProps, getErrorProps, getCounterProps }
Связка FormField: id каждой части, label указывает на контрол, его aria-describedby (описание или ошибка, счётчик), aria-invalid и aria-required. context — то, что читает useFormField внутри FormField.
useTabs
useTabs({ tabs, value?, defaultValue?, onValueChange?, orientation?, activation? }) → { value, getListProps, getTabProps, getPanelProps }
Tabs без разметки: связь id вкладки и панели, роуминговый tabindex, ←/→ (зеркально в RTL) или ↑/↓ для вертикальных, Home / End, автоматическая или ручная активация.
useMenu
useMenu({ items, open?, onClose?, onBack?, defaultHighlight?, floating?, … }) → { open, show, close, entries, activeIndex, submenu, getTriggerProps, getTargetProps, getMenuProps, getItemProps, getSubmenuOptions }
Уровень меню, общий для DropdownMenu и ContextMenu: действия, флажки и радио-строки, поиск по буквам, подменю (ещё один useMenu с getSubmenuOptions), кнопка меню или контекстная цель (правый клик, долгое нажатие, Shift+F10).
usePagination
usePagination({ pageCount, value?, defaultValue?, onValueChange?, siblings?, boundaries? }) → { page, range, setPage, getListProps, getPreviousProps, getNextProps, getPageProps }
Pagination без разметки: диапазон страниц с многоточиями (getPaginationRange, чистая функция), назад / вперёд, стрелки между кнопками, фокус не теряется, когда кнопка шага выключается.
getPaginationRange
getPaginationRange({ page, pageCount, siblings?, boundaries? }) → (number | "start-ellipsis" | "end-ellipsis")[]
Номера страниц и многоточия для показа; длина постоянна, когда список сворачивается, поэтому ширина не прыгает.
useDialog
useDialog({ open?, defaultOpen?, onOpenChange?, role?, modal?, layer? }) → { open, setOpen, titleId, descriptionId, getTriggerProps, getCloseProps, getDialogProps, getTitleProps, getDescriptionProps }
Dialog и Drawer без разметки: состояние, id заголовка и описания для имени диалога, ARIA кнопки-открывателя, а с layer — и сам модальный слой: ловушка фокуса, блокировка прокрутки, Escape и клик снаружи.
useTooltip
useTooltip({ open?, defaultOpen?, onOpenChange?, delay?, disabled?, floating?, placement? }) → { visible, show, hide, getTriggerProps, getTooltipProps }
Tooltip без разметки: наведение открывает с задержкой, фокус с клавиатуры — сразу, пауза, чтобы дойти до подсказки (WCAG 1.4.13), Escape, одна подсказка за раз и общий «прогрев», aria-describedby на триггере.
useResizablePanels
useResizablePanels({ panels, direction?, layout?, onLayout?, autoSaveId?, keyboardStep? }) → { layout, collapse, expand, resize, getGroupProps, getPanelProps, getHandleProps }
ResizablePanels без разметки: размеры в процентах с пределами в пикселях, сворачивание, сохранённая раскладка и ручка-разделитель (перетаскивание, стрелки, Home / End, Enter, двойной клик).
useDateField
useDateField({ value?, defaultValue?, onValueChange?, locale?, minDate?, maxDate?, … }) → { value, setValue, outOfRange, segments, getGroupProps, focusFirst }
На этом хуке построен DateField кита: день, месяц и год — сегменты-spinbutton в порядке локали (Intl formatToParts), ввод по цифре или стрелками, как DateField в React Aria. Выводите каждый элемент segments как span с его props.
useTimeField
useTimeField({ value?, defaultValue?, onValueChange?, hourCycle, locale?, … }) → { value, setValue, segments, getGroupProps, focusFirst }
То же для времени: час, минута и на 12-часовых часах AM/PM; значение — 24-часовое «HH:mm».
useCheckboxTree
useCheckboxTree({ nodes, value?, defaultValue?, onValueChange?, expanded?, defaultExpanded?, … }) → { value, visible, getCheckState, toggle, isExpanded, setExpanded, focus, getTreeProps, getItemProps, getRowProps, getToggleProps, getGroupProps }
На этом хуке построен CheckboxTree кита: отмеченные листья, ветви — отмечены, частично или нет по своим листьям, ветвь переключает свои доступные листья, раскрытые ветви, роуминговый tabindex и клавиши дерева (стрелки, в том числе в RTL, Home / End, Space / Enter).
useStepper
useStepper({ steps, value?, defaultValue?, onValueChange?, clickable? }) → { value, setValue, getStepState, isClickable, getStepProps, getTriggerProps }
Stepper без разметки: каждый шаг пройден, текущий, впереди или с ошибкой, aria-current="step", а с clickable пройденные шаги — кнопки возврата; шаг, к которому вернулись, получает фокус.
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 }
На этом хуке построен Calendar кита: одна дата или диапазон (конец виден под указателем, оба выбора озвучиваются), месяцы рядом, неделя с первого дня локали, min / max / недоступные дни и клавиши сетки дат (стрелки, PageUp / PageDown, Home / End). Новый месяц ставит data-turn на корень для CSS-перелистывания.
useSlider
useSlider({ value?, defaultValue?, onValueChange?, min?, max?, step?, disabled?, formatValue?, name?, … }) → { values, isRange, dragging, setThumbValue, getRootProps, getTrackProps, getRangeProps, getThumbProps, getInputProps, getThumbLabelProps, getHiddenInputProps }
Slider без разметки: одно значение или диапазон, ползунки которого не заходят друг за друга, нажатие хватает ближайший, клавиши (стрелки зеркально в RTL, PageUp / PageDown, Home / End) и скрытый нативный range-input на каждый ползунок, чтобы его меняли жесты VoiceOver и TalkBack.
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 без разметки: перетаскивание с копией под указателем (на тач-экране — долгим нажатием), Space / стрелки / Enter / Escape с клавиатуры, кнопка старта и кнопки перемещения для экранных дикторов и одного указателя (WCAG 2.5.7), озвучка каждого шага и плавный переход элементов на новое место.
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 без разметки (паттерн APG): трек со scroll-snap, страница которого следует за свайпами и прокруткой, назад / вперёд, клавиши на треке, перетаскивание мышью с броском, точки или миниатюры, автопрокрутка с паузой при наведении, фокусе и reduced motion и вежливая live-область.
usePopover
usePopover({ open?, defaultOpen?, onOpenChange?, floating?, placement?, offset? }) → { open, setOpen, triggerRef, contentRef, getTriggerProps, getContentProps }
Popover без разметки: кнопка открывает немодальный диалог, названный по кнопке, если у него нет своего имени, фокус уходит внутрь при открытии и возвращается при закрытии, Tab за край панели продолжает с места кнопки на странице. С floating хук позиционирует панель и закрывает её по клику снаружи и Escape.
useNumberInput
useNumberInput({ value?, defaultValue?, onValueChange?, min?, max?, step?, precision?, locale?, formatOptions?, allowWheel?, … }) → { value, inputValue, increment, decrement, getInputProps, getIncrementProps, getDecrementProps, getHiddenInputProps }
NumberInput без разметки: spinbutton в формате локали, ввод хранится черновиком и применяется на blur или Enter (с ограничением и округлением), ↑ / ↓ (Shift ×10), PageUp / PageDown, Home / End, колесо при фокусе и кнопки шага, которые ускоряются, пока их держат.
Ref, события и направление
mergeRefs
mergeRefs(...refs) → ref callback
Один ref на несколько: объектные ref и колбэки, с очисткой.
composeHandlers
composeHandlers(theirs, ours) → handler
Сначала вызывает обработчик пользователя и пропускает ваш, если тот вызвал preventDefault().
useLatest
useLatest(value) → ref
ref с последним значением — для эффектов и стабильных колбэков.
useEventCallback
useEventCallback(fn) → fn
Стабильная функция, которая всегда вызывает последнюю реализацию.
cx
cx('a', cond && 'b', undefined) → 'a b'
Склеивает классы, пропуская ложные значения.
useDirection
useDirection(ref) → 'ltr' | 'rtl'
Направление письма у элемента, перечитывается после каждого рендера.
getDirection
getDirection(element) → 'ltr' | 'rtl'
То же, прочитанное один раз из CSS (учитывает dir любого предка).
inlineArrowStep
inlineArrowStep(key, direction) → -1 | 0 | 1
ArrowLeft / ArrowRight как шаг вдоль строки, зеркально в RTL.
isComposingKey
isComposingKey(event) → boolean
true, пока идёт ввод через IME (японский, китайский, корейский): Enter и стрелки принадлежат ему.
clamp
clamp(value, min, max) → number
Ограничивает число диапазоном.
Помощники для React 17–19
useId
useId() → string
useId из React 18+; на React 17 id назначается сразу после монтирования (в серверном HTML его нет, гидрация совпадает).
useSyncExternalStore
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Из React 18+, на 17 — аналог на состоянии и эффектах.
useIsomorphicLayoutEffect
useIsomorphicLayoutEffect(effect, deps)
useLayoutEffect в браузере, useEffect на сервере (без предупреждения SSR).
withRef
withRef(render, statics?)
ref как проп на любой версии: сам render на React 19, forwardRef раньше. statics становятся частями (Card.Header).
cleanupRef
cleanupRef(node => cleanup) → ref callback
ref-колбэк, возвращающий очистку, как в React 19, — и на 17 и 18.
sharedCleanupRef
sharedCleanupRef(node => cleanup) → ref callback
cleanupRef для одного колбэка на много элементов (ref={measure} на каждой строке).
getElementRef
getElementRef(element) → ref
ref элемента: props.ref на React 19, element.ref раньше.
startTransition
startTransition(scope)
Из React 18+; на 17 обновление выполняется сразу.
useTransition
useTransition() → [isPending, startTransition]
Из React 18+; на 17 никогда не pending.
useDeferredValue
useDeferredValue(value) → value
Из React 18+; на 17 — само значение.