VibeUI
ComponentsDocs
VibeUI

Components with taste, that you own. Copy the source, retint the tokens, keep every line.

Library

  • All components
  • Signature pieces
  • Text effects
  • Footers

Docs

  • Introduction
  • Installation
  • Theming
  • CLI

Project

  • GitHub
  • Agents and MCP
  • Issues
  • MIT license
© 2026 VibeUI · MIT licensed
  1. Components
  2. Signature
  3. Magnet Tabs

Magnet Tabs

Tabs with a hover highlight that glides between items and a marker on the active tab.

SignatureClient component

Hover to glide · click to select (motion).

Install

bash
npx @agents-npm-packages/vibeui add magnet-tabs --dir ./src/components/ui

Import

tsx
import { MagnetTabs } from "@/components/ui/magnet-tabs"

API

Generated from the component's TypeScript types, so it always matches the source. * marks a required prop.

<MagnetTabs>

PropTypeDefaultDescription
tabs*{ value: string; label: string; }[]—Array of { value, label }.
value*string—Controlled value.
onChange*(value: string) => void—Called when the value changes.
classNamestring—Extra classes, merged with the component's own.

Source

src/components/ui/magnet-tabs.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";

/** Tabs with a hover highlight that glides between items and a marker on the active tab. */
export function MagnetTabs({
  tabs,
  value,
  onChange,
  className,
}: {
  tabs: { value: string; label: string }[];
  value: string;
  onChange: (value: string) => void;
  className?: string;
}) {
  const listRef = React.useRef<HTMLDivElement>(null);
  const [hover, setHover] = React.useState<{ left: number; width: number } | null>(null);

  const target = (el: HTMLElement) =>
    setHover({ left: el.offsetLeft, width: el.offsetWidth });

  return (
    <div
      ref={listRef}
      role="tablist"
      onMouseLeave={() => setHover(null)}
      className={cn("relative inline-flex gap-1", className)}
    >
      <span
        aria-hidden
        className={cn(
          "absolute inset-y-0 rounded-sm bg-surface-muted transition-[left,width,opacity] duration-300 ease-out",
          hover ? "opacity-100" : "opacity-0",
        )}
        style={{ left: hover?.left ?? 0, width: hover?.width ?? 0 }}
      />
      {tabs.map((tab) => {
        const selected = value === tab.value;
        return (
          <button
            key={tab.value}
            type="button"
            role="tab"
            aria-selected={selected}
            onClick={() => onChange(tab.value)}
            onMouseEnter={(e) => target(e.currentTarget)}
            onFocus={(e) => target(e.currentTarget)}
            className={cn(
              "relative z-10 h-9 px-3.5 text-[13px] font-medium transition-colors duration-200",
              selected ? "text-ink" : "text-muted hover:text-ink",
            )}
          >
            {tab.label}
            <span
              aria-hidden
              className={cn(
                "absolute inset-x-3.5 -bottom-px h-0.5 origin-center rounded-full bg-accent transition-transform duration-300 ease-out",
                selected ? "scale-x-100" : "scale-x-0",
              )}
            />
          </button>
        );
      })}
    </div>
  );
}

Exports MagnetTabs. Peer dependencies: clsx, tailwind-merge.

PreviousLoupeNextMarquee