Tabs

Tab component with animated highlight on hover.

JSX
import { useState, useRef } from 'react';

const tabData = [
  { label: 'About', pathname: '/about' },
  { label: 'Bookmarks', pathname: '/bookmarks' },
  { label: 'Snippets', pathname: '/snippets' },
  { label: 'Projects', pathname: '/projects' },
];

const Tabs = () => {
  const [tabBoundingBox, setTabBoundingBox] = useState(null);
  const [wrapperBoundingBox, setWrapperBoundingBox] = useState(null);
  const [highlightedTab, setHighlightedTab] = useState(null);
  const [isHoveredFromNull, setIsHoveredFromNull] = useState(true);

  const highlightRef = useRef(null);
  const wrapperRef = useRef(null);

  const repositionHighlight = (e, tab) => {
    setTabBoundingBox(e.target.getBoundingClientRect());
    setWrapperBoundingBox(wrapperRef.current.getBoundingClientRect());
    setIsHoveredFromNull(!highlightedTab);
    setHighlightedTab(tab);
  };

  const resetHighlight = () => setHighlightedTab(null);

  const highlightStyles = {};
  if (tabBoundingBox && wrapperBoundingBox) {
    highlightStyles.transitionDuration = isHoveredFromNull ? '0ms' : '150ms';
    highlightStyles.opacity = highlightedTab ? 1 : 0;
    highlightStyles.width = `${tabBoundingBox.width}px`;
    highlightStyles.transform = `translateX(${tabBoundingBox?.left - wrapperBoundingBox?.left}px)`;
  }

  return (
    <div className="relative" ref={wrapperRef} onMouseLeave={resetHighlight}>
      <div
        className="absolute left-0 top-1 rounded-md bg-gray-300 py-4 duration-150 dark:bg-gray-600"
        ref={highlightRef}
        style={{
          ...highlightStyles,
          transitionProperty: 'width, transform, opacity',
        }}
      />
      {tabData.map((tab) => (
        <a
          className="relative inline-block cursor-pointer p-2 text-gray-700 transition duration-100 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-300"
          key={tab.label}
          href={tab.pathname}
          onMouseOver={(ev) => repositionHighlight(ev, tab)}
        >
          {tab.label}
        </a>
      ))}
    </div>
  );
};

export default Tabs;
Last Updated: