Composables
Every component is a thin template over a handful of composables: press handling, focus management, collections, overlays, locale-aware formatting. The ones listed here are exported from ropav so you can build a control the library does not ship and have it behave like the rest.
import { useLocale, usePress } from "ropav";Most are ported from React Aria and React Stately, and keep their names, so their documentation is a useful second reference. Options that can change take a getter or a ref — () => props.isOpen — and state comes back as computed refs.
A component's own state engine — useSelectState, useCalendarState and the like — is not on this list. Those only make sense paired with the component's parts, and the types their props name are exported from the component instead.
Interactions
usePress
usePress(options?: UsePressOptions): UsePressReturnA press that behaves the same for a mouse, a finger, a keyboard and a screen reader: started on the way down, cancelled when the pointer leaves, and never fired twice for one click.
const press = usePress({
onPress: (event) => console.log(event.pointerType),
});
// <button :data-pressed="press.isPressed.value"
// @click="press.handlers.onClick" @pointerdown="press.handlers.onPointerdown" …>useLongPress
useLongPress(options?: UseLongPressOptions): UseLongPressReturnA press held past a threshold, built on usePress. The description it takes is announced to assistive technology so the gesture is discoverable.
const longPress = useLongPress({
accessibilityDescription: "Long press to open menu",
onLongPress: () => state.open("first"),
});useMove
useMove(options: UseMoveOptions): UseMoveReturnPointer drags and arrow keys, reported as a stream of deltas rather than coordinates. What a slider thumb, a splitter handle or a column resizer is made of.
const { handlers } = useMove({
onMove: ({ deltaX }) => {
offset.value += deltaX;
},
});
// <div @keydown="handlers.onKeydown" @pointerdown="handlers.onPointerdown" />useInteractionStates
useInteractionStates(options?: UseInteractionStatesOptions): UseInteractionStatesReturnHover, press, focus and focus-visible, ready to publish as data-* attributes. Hover ignores touch, and focus-visible only turns on for keyboard focus.
const states = useInteractionStates({ isDisabled: () => props.isDisabled });
// <button :data-hovered="states.isHovered.value" @pointerenter="states.onPointerenter" …>useFocusWithin
useFocusWithin(options?: UseFocusWithinOptions): UseFocusWithinReturnWhether focus is anywhere inside an element, for a wrapper that draws the ring around the control it contains.
const focus = useFocusWithin({ isDisabled: () => props.isDisabled });
// <div :data-focus-within="focus.isFocusWithin.value" @focusin="focus.onFocusin" …>useInteractionModality
useInteractionModality(): ComputedRef<"keyboard" | "pointer">How the user last drove the page, for text that has to change with it — "Press Enter to start dragging" against "Double tap to start dragging".
useTypeahead
useTypeahead(options: UseTypeaheadOptions): UseTypeaheadReturnMove focus by typing the start of an item's text. A leading Space selects rather than searches, as it does in a native list.
const typeahead = useTypeahead({
focusedKey: () => selection.focusedKey.value,
getKeyForSearch: (search, fromKey) => findMatch(search, fromKey),
onSearchMatch: (key) => selection.setFocusedKey(key),
});Trigger responders
<Dropdown><Button /></Dropdown> works because the trigger is an ordinary pressable that takes its press from above. These are that channel, so a control of your own can be a trigger too — or drive ropav's Button from an overlay of your own.
Attach the listeners with @event, never v-bind
attrs is safe to bind, but handlers is not. Vapor re-applies every on* key on each render, which reorders the listeners and can drop one mid-dispatch. Compose them with composePressResponder or composeFocusResponder and attach each with @event.
usePressResponder, providePressResponder
usePressResponder(): PressResponder | null
providePressResponder(responder: PressResponder | null): voidThe press, ARIA attributes and disabled state an overlay hands its trigger. null when nothing above supplies one.
composePressResponder
composePressResponder(responder: PressResponder | null, own?: Partial<UsePressHandlers>): UsePressHandlersThe responder's listeners chained ahead of the element's own, as stable functions.
const responder = usePressResponder();
const press = composePressResponder(responder, { onClick, onPointerdown });
// <button v-bind="responder?.attrs.value" @click="press.onClick" @pointerdown="press.onPointerdown" …>useFocusResponder, provideFocusResponder
useFocusResponder(): FocusResponder | null
provideFocusResponder(responder: FocusResponder | null): voidThe hover and focus a tooltip hands its trigger. Separate from the press responder, so a button can be a menu trigger and a tooltip trigger at once.
composeFocusResponder
composeFocusResponder(responder: FocusResponder | null, own?: Partial<FocusResponderHandlers>): FocusResponderHandlersconst focus = composeFocusResponder(useFocusResponder(), { onFocus, onBlur });
// <button @focus="focus.onFocus" @blur="focus.onBlur" …>Overlays
useOverlayTriggerState
useOverlayTriggerState(options?: UseOverlayTriggerStateOptions): OverlayTriggerStateOpen state for anything that opens, controlled or not.
const state = useOverlayTriggerState({
isOpen: () => props.isOpen,
onOpenChange: (isOpen) => emit("openChange", isOpen),
});useOverlayPosition
useOverlayPosition(options: UseOverlayPositionOptions): UseOverlayPositionReturnPlaces an overlay against its trigger, flips it when it would leave the viewport, and keeps it there through scrolling and resizing.
const { overlayStyle, placement } = useOverlayPosition({
isOpen: () => state.isOpen.value,
offset: 8,
overlayRef: popoverElement,
placement: "bottom start",
targetRef: triggerElement,
});useDismissable
useDismissable(options: UseDismissableOptions): UseDismissableReturnCloses an overlay on Escape or on an interaction outside it, and understands that a submenu opened from inside it is not outside. With a region, a press outside that element is neither a dismissal nor swallowed, and the overlay only stacks with overlays whose region overlaps its own.
const { onKeydown } = useDismissable({
isDismissable: true,
isOpen: () => state.isOpen.value,
onClose: state.close,
overlayRef: popoverElement,
});useFocusScope
useFocusScope(options: UseFocusScopeOptions): voidKeeps focus inside an overlay while it is open and gives it back to where it came from when it closes. With a region, focus moving outside that element is let go rather than pulled back.
useFocusScope({
autoFocus: true,
contain: true,
isActive: () => state.isOpen.value,
restoreFocus: true,
scopeRef: popoverElement,
});usePortal, providePortal
usePortal(): Portal
providePortal(options: PortalOptions): PortalWhere overlays render and the part of the page they belong to: the nearest provider's container and root, or "body" and null (the whole document) without one. Pass portal.container.value to a Teleport, and portal.root.value as the region of useDismissable and useFocusScope and the boundaryElement of useOverlayPosition. PortalProvider is providePortal as a component.
const portal = usePortal();
// <Teleport :to="portal.container.value">
// portal.isContained.value → true when the container is not the bodyusePreventScroll
usePreventScroll(options?: UsePreventScrollOptions): voidBlocks page scrolling while a modal is open, without the layout shift a disappearing scrollbar causes.
usePreventScroll({ isDisabled: () => !state.isOpen.value });useEnterExit
useEnterExit(options: UseEnterExitOptions): UseEnterExitReturnKeeps an element mounted until its exit animation has finished, and reports which phase it is in.
const { isEntering, isExiting, isPresent } = useEnterExit({
elementRef: popoverElement,
isOpen: () => state.isOpen.value,
});useToast
useToast(options: UseToastOptions): UseToastReturnThe accessibility wiring for one toast — its role, its labelling and its timer. ToastProvider covers rendering; reach for this when the toast itself has to be a component of your own.
Collections
useCollection
useCollection(options?: UseCollectionOptions): UseCollectionReturnThe ordered set of items a list, menu or tag group navigates over. Items register themselves as they mount, so the order follows the DOM.
useSelectionManager
useSelectionManager(options: UseSelectionManagerOptions): UseSelectionManagerReturnSelected and focused keys over a collection, with single, multiple and replace-on-click behaviour and disabled keys.
const collection = useCollection();
const selection = useSelectionManager({
collection,
onSelectionChange: (keys) => emit("update:selectedKeys", keys),
selectionMode: "multiple",
});useListKeyboard
useListKeyboard(options: UseListKeyboardOptions): UseListKeyboardReturnArrow keys, Home and End, Page Up and Down and select-all over a collection. Focus moves for real with a roving tabindex, or virtually through aria-activedescendant when it has to stay in an input.
const keyboard = useListKeyboard({
collection,
element: listElement,
selection,
});useListData
useListData<T>(options?: UseListDataOptions<T>): ListData<T>A list held in state, with the operations a collection usually needs — insert, remove, move, filter and selection.
const list = useListData({
getKey: (user) => user.id,
initialItems: users,
});
list.remove(id);useFilter
useFilter(options?: Intl.CollatorOptions): ComputedRef<Filter>startsWith, contains and endsWith that compare text the way the locale reads it. With sensitivity: "base", "cafe" finds "Café".
const filter = useFilter({ sensitivity: "base" });
// <AutocompleteFilter :filter="filter.contains">Drag and drop
useDragAndDrop
useDragAndDrop(options: DragAndDropOptions): UseDragAndDropReturnDrag and drop for a ListBox or a Table. Which halves are switched on follows from the handlers you pass.
const { dragAndDropHooks } = useDragAndDrop({
getItems: (keys) => [...keys].map((key) => ({ "text/plain": String(key) })),
onReorder: (event) => reorder(event.keys, event.target),
});
// <ListBox :drag-and-drop-hooks="dragAndDropHooks" …>useDrop
useDrop(options: UseDropOptions): UseDropReturnMakes any element a drop target, for the pointer and for the keyboard and screen-reader drag that ropav's collections start. DropZone is built on it.
const drop = useDrop({
onDrop: async (event) => upload(event.items),
ref: zoneElement,
});
// <div v-bind="drop.attrs.value" :data-drop-target="drop.isDropTarget.value" …>Fields and forms
useFieldIds, useFieldIdsContext, provideFieldIdsContext
useFieldIds(options?: UseFieldIdsOptions): UseFieldIdsReturn
useFieldIdsContext(): FieldIdsContext | null
provideFieldIdsContext(context: FieldIdsContext | null): voidMints the ids a field's label, description and error message take, and links only the ones that are actually rendered. Providing the context is what lets ropav's Label, Description and FieldError wire themselves to a control of your own.
const { context, describedBy, labelId } = useFieldIds();
provideFieldIdsContext(context);
// <div role="group" :aria-labelledby="labelId" :aria-describedby="describedBy">useFormValidationState
useFormValidationState<T>(options: UseFormValidationStateOptions<T>): FormValidationStateWhat a field's validation currently says, merging the isInvalid prop, a validate function, the surrounding Form's server errors and the browser's own verdict.
const validation = useFormValidationState<boolean>({
isInvalid: () => props.isInvalid,
name: () => props.name,
validate: () => props.validate,
validationBehavior: () => props.validationBehavior,
value: () => state.value,
});useFormReset
useFormReset<T>(element, initialValue, onReset, form?): voidPuts a control's state back to its default when its form is reset.
const input = useTemplateRef<HTMLInputElement>("input");
useFormReset(input, () => props.defaultSelected ?? false, setSelected);useDescription
useDescription(description: MaybeRefOrGetter<string | undefined>): UseDescriptionReturnA description for assistive technology only, attached through aria-describedby.
const { describedBy } = useDescription(() => "Long press to open menu");
// <button :aria-describedby="describedBy.value">Internationalisation
useLocale, provideLocale
useLocale(): ComputedRef<Locale>
provideLocale(locale: MaybeRefOrGetter<string | null | undefined>): ComputedRef<Locale>The locale that applies here, with its writing direction: the nearest ancestor's choice, or the browser's. I18nProvider is provideLocale as a component.
const locale = useLocale();
// locale.value → { locale: "en-US", direction: "ltr" }useDateFormatter
useDateFormatter(options?: MaybeRefOrGetter<DateFormatterOptions>): ComputedRef<DateFormatter>A date formatter for the current locale, cached across the components that ask for the same options.
const formatter = useDateFormatter({ dateStyle: "long" });
// formatter.value.format(date)useNumberFormatter
useNumberFormatter(options?: MaybeRefOrGetter<NumberFormatOptions>): ComputedRef<Intl.NumberFormat>const percent = useNumberFormatter({ style: "percent" });useCollator
useCollator(options?: Intl.CollatorOptions): ComputedRef<Intl.Collator>A collator for sorting and comparing text in the current locale.
useLocalizedStringFormatter
useLocalizedStringFormatter(strings: LocalizedStrings): ComputedRef<LocalizedStringFormatter>Your own translated strings, read in the current locale. A string that needs variables is written as a function of the ones passed to format.
const strings = useLocalizedStringFormatter({
"en-US": { remove: (args) => `Remove ${args?.name}` },
"fr-FR": { remove: (args) => `Supprimer ${args?.name}` },
});
// strings.value.format("remove", { name })State and environment
useControllableState
useControllableState<T>(options: UseControllableStateOptions<T>): UseControllableStateReturn<T>A value that a parent may control or leave alone — the v-model half of every component.
const { setState, state } = useControllableState<boolean>({
defaultValue: false,
onValueChange: (open) => emit("update:open", open),
value: () => props.open,
});useId
useId(idOverride?: MaybeRefOrGetter<string | undefined>): ComputedRef<string>A stable id that a caller-supplied one overrides.
const triggerId = useId(() => props.id);useMediaQuery
useMediaQuery(query: MaybeRefOrGetter<string>, options?: UseMediaQueryOptions): ComputedRef<boolean>const isMobile = useMediaQuery("(max-width: 768px)");useViewportSize
useViewportSize(): ShallowRef<ViewportSize>The visual viewport, which on a phone is what is left above the software keyboard rather than the window.
const viewport = useViewportSize();
// style: `--visual-viewport-height: ${viewport.value.height}px`