Skip to content
All Taste components

Tabs

Pill tabs

Rounded filter tabs with counts that switch the list underneath, in a solid and a soft style.

solid · soft · counts · switching panels

  • Product designerRemote · Full time
    Design
  • Senior frontend engineerLisbon or remote
    Engineering
  • Support leadDublin · Full time
    Support
  • Product designerRemote · Full time
    Design
  • Brand designerLisbon · Contract
    Design
  • Senior frontend engineerLisbon or remote
    Engineering
  • Backend engineerRemote · Full time
    Engineering
  • Mobile engineerBerlin · Full time
    Engineering
  • Support leadDublin · Full time
    Support

Soft

"use client";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";

type Tab = { id: string; label: string; count?: number; content: React.ReactNode };

export function PillTabs({ tabs, variant = "solid", label }:
  { tabs: Tab[]; variant?: "solid" | "soft"; label: string }) {
  const [active, setActive] = useState(0);
  const refs = useRef<(HTMLButtonElement | null)[]>([]);

  const onKeyDown = (e: React.KeyboardEvent) => {
    const last = tabs.length - 1;
    const keys: Record<string, number> = {
      ArrowRight: active === last ? 0 : active + 1,
      ArrowLeft: active === 0 ? last : active - 1,
      Home: 0,
      End: last,
    };
    const next = keys[e.key];
    if (next === undefined) return;
    e.preventDefault();
    setActive(next);
    refs.current[next]?.focus();
  };

  return (
    <div className="grid gap-3.5 font-sans">
      <div role="tablist" aria-label={label} className="flex flex-wrap gap-1.5">
        {tabs.map((tab, i) => {
          const selected = active === i;
          return (
            <button
              key={tab.id}
              ref={(el) => { refs.current[i] = el; }}
              id={"pill-tab-" + tab.id}
              type="button"
              role="tab"
              aria-selected={selected}
              aria-controls={"pill-panel-" + tab.id}
              tabIndex={selected ? 0 : -1}
              onClick={() => setActive(i)}
              onKeyDown={onKeyDown}
              className={cn(
                "inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full border px-3.5 text-[13px] font-medium transition-colors",
                "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
                variant === "solid"
                  ? selected
                    ? "border-fg bg-fg text-bg"
                    : "border-border bg-bg text-fg-muted hover:border-border-strong hover:text-fg"
                  : selected
                    ? "border-transparent bg-primary-soft text-primary-soft-fg"
                    : "border-transparent text-fg-muted hover:bg-surface-2 hover:text-fg",
              )}
            >
              {tab.label}
              {tab.count !== undefined && (
                <span className={cn("text-[11.5px] tabular-nums", selected ? "opacity-70" : "text-fg-subtle")}>{tab.count}</span>
              )}
            </button>
          );
        })}
      </div>
      {tabs.map((tab, i) => (
        <div
          key={tab.id}
          id={"pill-panel-" + tab.id}
          role="tabpanel"
          aria-labelledby={"pill-tab-" + tab.id}
          hidden={active !== i}
          tabIndex={0}
          className="focus-visible:outline-none"
        >
          {tab.content}
        </div>
      ))}
    </div>
  );
}

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