Interaction Choreography

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.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add roving-highlight-system.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

Active edited usageDefault props
<RovingHighlight/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/roving-highlight-system.en.json

Or, 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

PropTypeDefaultDescription
valuestring | null—Controlled selected value
defaultValuestring | nullnullInitial 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
loopbooleantrueWhether navigation wraps around to the other end at the boundaries
selectOnFocusbooleanfalseWhether moving focus onto an item also selects it
childrenReact.ReactNode—Any containers and the RovingHighlightItems inside them

RovingHighlightItem

PropTypeDefaultDescription
valuestring—This item's unique value (must be unique within the set)
disabledbooleanfalseDisabled: 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)
childrenReact.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 layoutId shared layout animation takes over so the highlight morphs smoothly across different containers.
  • Namespace isolation: each RovingHighlight names its layoutId through a LayoutGroup, so multiple instances on the same page never pair their highlights with each other.
  • Three highlight styles: solid filled surface, outline ring, underline — all of them travel.

Accessibility

  • Roving tabindex: the whole set has exactly one tab stop (focus > selection > first available item); Tab enters, arrow keys roam across every registered item, Home / End jump to the first and last, Enter / Space select.
  • 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 carry aria-disabled and 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.