Web InnoventixFreeCode

List Highlight Hover Effect

Original · free

A list where the hovered row gets a sliding highlight and arrow.

byWeb InnoventixReact + Tailwind
hoverlisthighlight
Open

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
hover-list-highlight.tsx
"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

motion

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 quote

Similar components

Browse all →