Skip to content
All Taste components

Tabs

Underline tabs with counts

Underlined tabs that carry a count or a new-activity dot, for inboxes and review queues.

count badge · new dot · active underline · switching panels

  • Refund for order #2041Opened by Ana · 2h ago
    Waiting
  • Change billing emailOpened by Kai · 5h ago
    New
  • Annual plan discountAssigned to you · due today
    Review
  • Add a second adminAssigned to Mo · due Fri
    Review
  • Upgrade to Team planApproved by Lena · yesterday
    Done
  • Export March invoicesApproved by you · Mon
    Done
  • Ana mentioned you“Can you check the refund amount?”
    New
  • Kai replied“Sent the new email address.”
    Read
"use client";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";

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

export function CountTabs({ tabs, label }: { tabs: Tab[]; 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 font-sans">
      <div role="tablist" aria-label={label} className="flex gap-5 overflow-x-auto shadow-[inset_0_-1px_0] shadow-border [scrollbar-width:none]">
        {tabs.map((tab, i) => {
          const selected = active === i;
          return (
            <button
              key={tab.id}
              ref={(el) => { refs.current[i] = el; }}
              id={"count-tab-" + tab.id}
              type="button"
              role="tab"
              aria-selected={selected}
              aria-controls={"count-panel-" + tab.id}
              tabIndex={selected ? 0 : -1}
              onClick={() => setActive(i)}
              onKeyDown={onKeyDown}
              className={cn(
                "relative inline-flex h-[42px] shrink-0 items-center gap-2 whitespace-nowrap text-[13.5px] font-medium transition-colors",
                "after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-t-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
                selected ? "text-fg after:bg-primary" : "text-fg-muted hover:text-fg hover:after:bg-border-strong",
              )}
            >
              {tab.label}
              {tab.count !== undefined && (
                <span className={cn(
                  "grid h-5 min-w-5 place-items-center rounded-full px-1.5 text-[11.5px] tabular-nums",
                  selected ? "bg-primary-soft text-primary-soft-fg" : "bg-surface-2 text-fg-muted",
                )}>
                  {tab.count}
                </span>
              )}
              {tab.unread && <span className="size-[7px] rounded-full bg-danger ring-2 ring-bg" aria-label="New activity" role="img" />}
            </button>
          );
        })}
      </div>
      {tabs.map((tab, i) => (
        <div key={tab.id} id={"count-panel-" + tab.id} role="tabpanel" aria-labelledby={"count-tab-" + tab.id} hidden={active !== i} tabIndex={0}>
          {tab.content}
        </div>
      ))}
    </div>
  );
}

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