All Taste components
Tabs
Closeable tabs
Editor-style tabs for open files, each with a close button, an unsaved dot and a button to open another.
active top bar · close on hover · unsaved dot · add button · switching file panels
import { Button } from "./button"; export default function Page() { return <Button>Book a table</Button>;}
// Unsaved: new size propexport function Button({ size = "md", ...props }) { return <button data-size={size} {...props} />;}
:root { --radius: 10px; --font: "Inter", sans-serif;}
"use client";
import { useRef, useState } from "react";
import { FileCode2, Plus, X } from "lucide-react";
import { cn } from "@/lib/utils";
type Doc = { id: string; name: string; dirty?: boolean; body: string };
export function CloseableTabs({ initial, onAdd }: { initial: Doc[]; onAdd?: () => Doc }) {
const [docs, setDocs] = useState(initial);
const [activeId, setActiveId] = useState(initial[0]?.id);
const refs = useRef<Record<string, HTMLButtonElement | null>>({});
const index = docs.findIndex((d) => d.id === activeId);
const focus = (id: string) => { setActiveId(id); refs.current[id]?.focus(); };
const close = (id: string) => {
const i = docs.findIndex((d) => d.id === id);
const rest = docs.filter((d) => d.id !== id);
setDocs(rest);
if (id === activeId) setActiveId(rest[Math.min(i, rest.length - 1)]?.id);
};
const add = () => {
if (!onAdd) return;
const doc = onAdd();
setDocs((d) => [...d, doc]);
setActiveId(doc.id);
};
const onKeyDown = (e: React.KeyboardEvent) => {
const last = docs.length - 1;
const keys: Record<string, number> = {
ArrowRight: index === last ? 0 : index + 1,
ArrowLeft: index <= 0 ? last : index - 1,
Home: 0,
End: last,
};
if (e.key === "Delete" && activeId) { e.preventDefault(); close(activeId); return; }
const next = keys[e.key];
if (next === undefined || !docs[next]) return;
e.preventDefault();
focus(docs[next].id);
};
return (
<div className="overflow-hidden rounded-lg border border-border bg-bg font-sans">
<div className="flex items-stretch bg-surface shadow-[inset_0_-1px_0] shadow-border">
<div role="tablist" aria-label="Open files" className="flex min-w-0 overflow-x-auto [scrollbar-width:none]">
{docs.map((d) => {
const selected = d.id === activeId;
return (
<div key={d.id} role="presentation" className={cn("group relative flex shrink-0 items-center border-r border-border", selected && "bg-bg before:absolute before:inset-x-0 before:top-0 before:h-0.5 before:bg-primary")}>
<button
ref={(el) => { refs.current[d.id] = el; }}
id={"file-tab-" + d.id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"file-panel-" + d.id}
tabIndex={selected ? 0 : -1}
onClick={() => setActiveId(d.id)}
onKeyDown={onKeyDown}
className={cn("flex h-[38px] items-center gap-1.5 whitespace-nowrap pl-3 pr-1 text-[12.5px] font-medium focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring", selected ? "text-fg" : "text-fg-muted group-hover:text-fg")}
>
<FileCode2 className={cn("size-3.5", selected ? "text-primary" : "text-fg-subtle")} aria-hidden />
{d.name}
</button>
<button
type="button"
aria-label={"Close " + d.name + (d.dirty ? " (unsaved changes)" : "")}
onClick={() => close(d.id)}
className={cn(
"relative mr-1.5 grid size-[22px] place-items-center rounded-sm text-fg-subtle hover:bg-surface-2 hover:text-fg",
!selected && !d.dirty && "opacity-0 focus-visible:opacity-100 group-hover:opacity-100",
)}
>
{d.dirty ? (
<>
<span className="size-2 rounded-full bg-fg-muted group-hover:hidden" aria-hidden />
<X className="hidden size-[13px] group-hover:block" aria-hidden />
</>
) : (
<X className="size-[13px]" aria-hidden />
)}
</button>
</div>
);
})}
</div>
{onAdd && (
<button type="button" aria-label="Open a file" onClick={add} className="grid w-[38px] shrink-0 place-items-center text-fg-muted hover:bg-surface-2 hover:text-fg">
<Plus className="size-[15px]" aria-hidden />
</button>
)}
</div>
{docs.map((d) => (
<pre
key={d.id}
id={"file-panel-" + d.id}
role="tabpanel"
aria-labelledby={"file-tab-" + d.id}
hidden={d.id !== activeId}
tabIndex={0}
className="overflow-x-auto px-4 py-3 font-mono text-[12.5px] leading-7 text-fg"
>
{d.body}
</pre>
))}
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.