Skip to content
All Taste components

Tabs

Folder tabs

Tabs that sit on top of their panel like folder tabs, so the open one joins the card below.

joined active tab · icons · 3 panels (preview, code, notes)

Invoice INV-1042 · Kestrel RowPaid
Brand workshop, 2 days€2,400.00
Travel€180.00
VAT 23%€593.40
Total€3,173.40
{
  "number": "INV-1042",
  "client": "Kestrel Row",
  "total": 3173.40,
  "status": "paid"  // 2 Apr
}
Shown at the bottom of the invoice.
"use client";
import { useRef, useState } from "react";
import { Code2, FileText, Mail, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";

type Tab = { id: string; label: string; icon: LucideIcon; content: React.ReactNode };

export function FolderTabs({ 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="font-sans">
      <div role="tablist" aria-label={label} className="relative z-10 flex items-end gap-1 overflow-x-auto px-3 [scrollbar-width:none]">
        {tabs.map(({ id, label: text, icon: Icon }, i) => {
          const selected = active === i;
          return (
            <button
              key={id}
              ref={(el) => { refs.current[i] = el; }}
              id={"folder-tab-" + id}
              type="button"
              role="tab"
              aria-selected={selected}
              aria-controls={"folder-panel-" + id}
              tabIndex={selected ? 0 : -1}
              onClick={() => setActive(i)}
              onKeyDown={onKeyDown}
              className={cn(
                "-mb-px inline-flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-t border border-b-0 px-3.5 text-[13px] font-medium transition-colors",
                "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
                selected ? "h-[39px] border-border bg-bg text-fg" : "h-[34px] border-transparent bg-surface-2 text-fg-muted hover:text-fg",
              )}
            >
              <Icon className="size-[15px]" aria-hidden />
              {text}
            </button>
          );
        })}
      </div>
      <div className="rounded-lg border border-border bg-bg p-[18px] shadow-sm">
        {tabs.map((tab, i) => (
          <div key={tab.id} id={"folder-panel-" + tab.id} role="tabpanel" aria-labelledby={"folder-tab-" + tab.id} hidden={active !== i} tabIndex={0}>
            {tab.content}
          </div>
        ))}
      </div>
    </div>
  );
}

// Example
export function InvoiceTabs() {
  return (
    <FolderTabs
      label="Invoice"
      tabs={[
        { id: "preview", label: "Preview", icon: FileText, content: <p className="text-sm text-fg-muted">Invoice INV-1042</p> },
        { id: "json", label: "JSON", icon: Code2, content: <pre className="rounded bg-surface p-3 text-xs">{'{ "number": "INV-1042" }'}</pre> },
        { id: "notes", label: "Notes", icon: Mail, content: <p className="text-sm text-fg-muted">Note to client</p> },
      ]}
    />
  );
}

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