Skip to content
All Taste components

Micro-interactions

Sliding tabs

Tabs whose highlight glides to the one you pick, with the panel below fading in to match.

segmented · underline · panels

Checkout redesign3 screens in review, due Friday.
12 updates todayAsha approved the payment step.
5 peopleAsha, Dev, Meera, Kabir and you.
"use client";

import { useState } from "react";
import { cn } from "@/lib/utils";

interface SlidingTabsProps {
  tabs: { id: string; label: string; content?: React.ReactNode }[];
  variant?: "segmented" | "underline";
  label: string;
}

export function SlidingTabs({ tabs, variant = "segmented", label }: SlidingTabsProps) {
  const [active, setActive] = useState(0);
  const seg = variant === "segmented";
  const width = seg ? "calc((100% - 6px) / " + tabs.length + ")" : 100 / tabs.length + "%";

  return (
    <div className="grid w-full max-w-[300px] gap-3.5">
      <div
        role="tablist"
        aria-label={label}
        className={cn("relative isolate grid auto-cols-fr grid-flow-col", seg ? "rounded-lg bg-surface-2 p-[3px]" : "border-b border-border")}
      >
        {tabs.map((t, i) => (
          <button
            key={t.id}
            type="button"
            role="tab"
            id={t.id + "-tab"}
            aria-controls={t.id + "-panel"}
            aria-selected={i === active}
            onClick={() => setActive(i)}
            className={cn(
              "relative z-10 font-medium transition-colors motion-reduce:transition-none",
              seg ? "h-8 rounded text-[13px]" : "h-[38px] text-[13.5px]",
              i === active ? "text-fg" : "text-fg-muted hover:text-fg",
            )}
          >
            {t.label}
          </button>
        ))}
        <span
          aria-hidden
          className={cn(
            "absolute transition-transform duration-500 ease-spring motion-reduce:transition-none",
            seg ? "inset-y-[3px] left-[3px] rounded bg-bg shadow-sm" : "-bottom-px left-0 h-0.5 rounded-sm bg-primary",
          )}
          style={{ width, transform: "translateX(" + active * 100 + "%)" }}
        />
      </div>
      {tabs[active].content && (
        <div key={active} role="tabpanel" id={tabs[active].id + "-panel"} aria-labelledby={tabs[active].id + "-tab"} className="animate-fade text-[13.5px] text-fg-muted motion-reduce:animate-none">
          {tabs[active].content}
        </div>
      )}
    </div>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.