ValyroUI English version

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 — само значение.