"use client";
import { Button, Stack, Text, useRovingFocus } from "@vortexlabs/vortex";
import React from "react";
export const RovingFocusExample = () => {
const ref = React.useRef<HTMLDivElement>(null);
useRovingFocus({ ref, orientation: "horizontal", circular: true });
return (
<Stack gap={4}>
<Text>Focus a button, then use Left and Right to move between them.</Text>
<div
ref={ref}
role="toolbar"
aria-label="Focus navigation example"
style={{ display: "flex", gap: "var(--vortex-space-200)", flexWrap: "wrap" }}
>
<Button variant="secondary">First</Button>
<Button variant="secondary">Second</Button>
<Button variant="secondary">Third</Button>
</div>
</Stack>
);
};Features
- Keeps one control in the group’s tab order.
- Supports horizontal, vertical, and wrapping navigation.
Pass the container ref and navigation options. The hook returns nothing and moves focus only; your controls own their actions and selection.
Orientation and Wrapping
Horizontal mode uses Left and Right; vertical mode uses Up and Down. Omit orientation to accept both pairs, and set circular to wrap from one end to the other.
"use client";
import { Button, Stack, Text, useRovingFocus } from "@vortexlabs/vortex";
import React from "react";
export const VerticalToolbarExample = () => {
const ref = React.useRef<HTMLDivElement>(null);
const [action, setAction] = React.useState("None");
useRovingFocus({ ref, orientation: "vertical", circular: false });
return (
<Stack gap={4}>
<div
ref={ref}
role="toolbar"
aria-label="Object alignment"
aria-orientation="vertical"
style={{ display: "flex", flexDirection: "column", gap: "var(--vortex-space-200)" }}
>
<Button variant="secondary" onClick={() => setAction("Align top")}>
Align top
</Button>
<Button variant="secondary" onClick={() => setAction("Align center")}>
Align center
</Button>
<Button variant="secondary" onClick={() => setAction("Align bottom")}>
Align bottom
</Button>
</div>
<Text size="body-sm-desktop">Last action: {action}</Text>
</Stack>
);
};Horizontal arrows follow the focused group's text direction, including a nested dir="rtl" group.
Initial Tab Stop
Set initial tabIndex values to choose the entry control. The first eligible control whose index is not -1 becomes the tab stop.
"use client";
import { Button, Stack, Text, useRovingFocus } from "@vortexlabs/vortex";
import React from "react";
export const RovingInitialExample = () => {
const ref = React.useRef<HTMLDivElement>(null);
const [zoom, setZoom] = React.useState(100);
useRovingFocus({ ref, orientation: "horizontal" });
return (
<Stack gap={4}>
<div
ref={ref}
role="toolbar"
aria-label="Zoom presets"
style={{ display: "flex", gap: "var(--vortex-space-200)" }}
>
<Button
variant="secondary"
attributes={{ tabIndex: -1, "aria-pressed": zoom === 50 }}
onClick={() => setZoom(50)}
>
50%
</Button>
<Button
variant="secondary"
attributes={{ tabIndex: 0, "aria-pressed": zoom === 100 }}
onClick={() => setZoom(100)}
>
100%
</Button>
<Button
variant="secondary"
attributes={{ tabIndex: -1, "aria-pressed": zoom === 200 }}
onClick={() => setZoom(200)}
>
200%
</Button>
</div>
<Text size="body-sm-desktop">Zoom: {zoom}%</Text>
</Stack>
);
};Use controlledTabStop: true to manage indexes yourself. Keep one control at 0 and the others at -1.
Disabling Navigation
Set disabled: true to stop navigation. Existing tab indexes are not restored; reset them yourself when switching to ordinary Tab navigation.
useRovingFocus({ ref, disabled: true });