Roving Highlight System
One shared morphing highlight surface that travels between any registered elements across tabs, sidebars, and grids, with built-in roving tabindex keyboard navigation and a synchronized focus ring.
npx shadcn@latest add https://webberui.com/r/roving-highlight-system.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<RovingHighlightSystem />
Installation
npx shadcn@latest add https://webberui.com/r/roving-highlight-system.jsonOr, once registries are configured in components.json, install it as @webberui/roving-highlight-system.
Usage
Wrap any containers and the RovingHighlightItems inside them in a single RovingHighlight, and the whole set shares one travelling highlight. When the selection crosses from one container to another, the highlight surface morphs smoothly across to the new position.
import {
RovingHighlight,
RovingHighlightItem,
} from "@/components/ui/roving-highlight-system";
<RovingHighlight defaultValue="overview" variant="solid">
{/* Container one: tab strip */}
<div className="flex gap-1">
<RovingHighlightItem value="overview">Overview</RovingHighlightItem>
<RovingHighlightItem value="activity">Activity</RovingHighlightItem>
</div>
{/* Container two: sidebar — the highlight travels all the way here from the tab strip */}
<div className="flex flex-col gap-1">
<RovingHighlightItem value="inbox">Inbox</RovingHighlightItem>
<RovingHighlightItem value="starred">Starred</RovingHighlightItem>
</div>
</RovingHighlight>;Controlled mode
Pass value and onValueChange to enter controlled mode, where the selection state is owned by the outside:
const [value, setValue] = React.useState("overview");
<RovingHighlight value={value} onValueChange={setValue}>
{/* ... */}
</RovingHighlight>;Selection follows focus
With selectOnFocus on, moving focus with the arrow keys also selects (automatic activation), so the highlight tracks focus directly:
<RovingHighlight defaultValue="overview" selectOnFocus>
{/* ... */}
</RovingHighlight>Props
RovingHighlight
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | null | — | Controlled selected value |
defaultValue | string | null | null | Initial selected value in uncontrolled mode |
onValueChange | (value: string) => void | — | Fires when the selection changes |
variant | "solid" | "outline" | "underline" | "solid" | Highlight style: filled surface / outline ring / underline |
orientation | "horizontal" | "vertical" | "both" | "both" | Arrow-key navigation direction |
loop | boolean | true | Whether navigation wraps around to the other end at the boundaries |
selectOnFocus | boolean | false | Whether moving focus onto an item also selects it |
children | React.ReactNode | — | Any containers and the RovingHighlightItems inside them |
RovingHighlightItem
| Prop | Type | Default | Description |
|---|---|---|---|
value | string | — | This item's unique value (must be unique within the set) |
disabled | boolean | false | Disabled: not selectable, skipped in keyboard navigation, never a tab stop |
onSelect | (value: string) => void | — | Fires when it is selected (by click or keyboard Enter/Space) |
children | React.ReactNode | — | Item content (text, icons, and so on) |
All other native button attributes (aria-label, onClick, and so on) are forwarded to the underlying element.
How it works
- Travelling across containers: only the selected item in the whole set renders the highlight surface; on switch, the old position unmounts and the new one mounts, and Motion's
layoutIdshared layout animation takes over so the highlight morphs smoothly across different containers. - Namespace isolation: each
RovingHighlightnames itslayoutIdthrough aLayoutGroup, so multiple instances on the same page never pair their highlights with each other. - Three highlight styles:
solidfilled surface,outlinering,underline— all of them travel.
Accessibility
- Roving tabindex: the whole set has exactly one tab stop (focus > selection > first available item);
Tabenters, arrow keys roam across every registered item,Home/Endjump to the first and last,Enter/Spaceselect. - Focus ring synchronization: a
:focus-visible-like detection shows the focus ring only for keyboard operation; the focus ring is likewise a shared element and morphs across containers as focus moves. - The selected item carries
aria-current="true"; disabled items carryaria-disabledand are excluded from keyboard navigation. - When the user has "reduce motion" enabled at the system level, the highlight and focus ring simply appear at their new position instead of travelling.
Focus Recession Stage
A page-level focus system — when any region enters focus/edit mode, the rest of the layout recedes as a whole: scaled down, dimmed, and blurred into a depth-of-field backdrop, springing back on Esc or an outside click.
Column Transit Board
A cross-column transit board — cards fly along an arc between columns because of a programmatic state change (not a drag), while the target column opens a landing spot, neighbours in the source column close ranks, and the flying card lifts with a shadow.