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.