List Highlight Hover Effect
Original · freeA list where the hovered row gets a sliding highlight and arrow.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/hover-list-highlight.json"use client";
import { useCallback, useEffect, useLayoutEffect, useRef, useState, type KeyboardEvent } from "react";
import { useReducedMotion } from "motion/react";
type Item = {
title: string;
meta: string;
href: string;
};
const ITEMS: Item[] = [
{
title: "Conversion-first landing pages",
meta: "CRO",
href: "#landing-pages",
},
{
title: "Technical SEO & Core Web Vitals",
meta: "SEO",
href: "#technical-seo",
},
{
title: "Headless commerce storefronts",
meta: "Commerce",
href: "#headless-commerce",
},
{
title: "Design systems in code",
meta: "Systems",
href: "#design-systems",
},
{
title: "Analytics & experiment tracking",
meta: "Data",
href: "#analytics",
},
{
title: "AI answer-engine optimization",
meta: "AIEO",
href: "#aieo",
},
];
export default function HoverListHighlight() {
const reduceMotion = useReducedMotion();
const listRef = useRef<HTMLUListElement | null>(null);
const itemRefs = useRef<Array<HTMLAnchorElement | null>>([]);
const [active, setActive] = useState<number | null>(null);
const [rect, setRect] = useState<{ top: number; height: number } | null>(null);
const measure = useCallback((index: number | null) => {
if (index === null) {
setRect(null);
return;
}
const list = listRef.current;
const el = itemRefs.current[index];
if (!list || !el) return;
const listBox = list.getBoundingClientRect();
const elBox = el.getBoundingClientRect();
setRect({ top: elBox.top - listBox.top, height: elBox.height });
}, []);
useLayoutEffect(() => {
measure(active);
}, [active, measure]);
useEffect(() => {
if (active === null) return;
const onResize = () => measure(active);
window.addEventListener("resize", onResize);
return () => window.removeEventListener("resize", onResize);
}, [active, measure]);
const onKeyNav = useCallback(
(e: KeyboardEvent<HTMLAnchorElement>, index: number) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
const dir = e.key === "ArrowDown" ? 1 : -1;
const next = (index + dir + ITEMS.length) % ITEMS.length;
itemRefs.current[next]?.focus();
setActive(next);
} else if (e.key === "Home") {
e.preventDefault();
itemRefs.current[0]?.focus();
setActive(0);
} else if (e.key === "End") {
e.preventDefault();
const last = ITEMS.length - 1;
itemRefs.current[last]?.focus();
setActive(last);
}
},
[]
);
const highlightVisible = active !== null && rect !== null;
return (
<section className="relative w-full bg-white px-6 py-20 text-slate-900 sm:py-28 dark:bg-slate-950 dark:text-slate-100">
<style>{`
@keyframes hlh_arrow_in {
from { opacity: 0; transform: translateX(-6px); }
to { opacity: 1; transform: translateX(0); }
}
@media (prefers-reduced-motion: reduce) {
.hlh-anim { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-2xl">
<div className="mb-10">
<span className="inline-flex items-center rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium tracking-wide text-indigo-700 uppercase dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
What we build
</span>
<h2 className="mt-4 text-2xl font-semibold tracking-tight sm:text-3xl">
Capabilities, end to end
</h2>
<p className="mt-2 text-sm text-slate-500 dark:text-slate-400">
Hover or use arrow keys to move through the list.
</p>
</div>
<ul
ref={listRef}
className="relative isolate flex flex-col"
onMouseLeave={() => setActive(null)}
>
{/* sliding highlight */}
<div
aria-hidden="true"
className={
"pointer-events-none absolute right-0 left-0 -z-10 rounded-xl bg-indigo-50 ring-1 ring-indigo-100 dark:bg-indigo-500/10 dark:ring-indigo-500/20 " +
(reduceMotion
? ""
: "transition-[transform,height,opacity] duration-300 ease-out")
}
style={{
opacity: highlightVisible ? 1 : 0,
height: rect ? rect.height : 0,
transform: `translateY(${rect ? rect.top : 0}px)`,
}}
/>
{ITEMS.map((item, i) => {
const isActive = active === i;
return (
<li key={item.href} className="relative">
<a
ref={(el) => {
itemRefs.current[i] = el;
}}
href={item.href}
onMouseEnter={() => setActive(i)}
onFocus={() => setActive(i)}
onKeyDown={(e) => onKeyNav(e, i)}
className="group flex items-center gap-4 rounded-xl px-4 py-4 outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950"
>
{/* arrow */}
<span
aria-hidden="true"
className={
"flex h-5 w-5 shrink-0 items-center justify-center text-indigo-600 dark:text-indigo-400 " +
(isActive ? "hlh-anim" : "") +
" " +
(isActive
? reduceMotion
? "opacity-100"
: "opacity-100"
: "opacity-0")
}
style={
isActive && !reduceMotion
? { animation: "hlh_arrow_in 260ms ease-out both" }
: undefined
}
>
<svg
viewBox="0 0 20 20"
fill="none"
className="h-5 w-5"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M4 10h11" />
<path d="M11 6l4 4-4 4" />
</svg>
</span>
<span
className={
"min-w-0 flex-1 truncate text-base font-medium transition-colors duration-200 " +
(isActive
? "text-indigo-700 dark:text-indigo-200"
: "text-slate-700 dark:text-slate-300")
}
>
{item.title}
</span>
<span
className={
"shrink-0 rounded-md px-2 py-0.5 text-xs font-semibold tracking-wide uppercase transition-colors duration-200 " +
(isActive
? "bg-indigo-100 text-indigo-700 dark:bg-indigo-500/20 dark:text-indigo-200"
: "bg-slate-100 text-slate-500 dark:bg-slate-800 dark:text-slate-400")
}
>
{item.meta}
</span>
</a>
</li>
);
})}
</ul>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Tilt Hover Effect
OriginalA card that tilts in 3D toward the cursor on hover.

Card Spotlight Hover Effect
OriginalA card with a cursor-following radial spotlight.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Fill Hover Effect
OriginalIcon buttons whose icon and background fill on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

