All Taste components
Tabs
Vertical tabs
A settings layout with tabs down the side and the chosen section beside them, turning into a top row on narrow screens.
side list with icons · section label · 5 panels with fields, members and switches · stacks when narrow
Workspace
General
How your workspace appears to clients.
Members
3 of 5 seats used.
Leah Morenoleah@loft.example
Jun Satojun@loft.example
Rita Banksrita@loft.example
Billing
Team plan, billed monthly.
Team · 5 seatsRenews on 1 May
Visa ending 4242Expires 08/28
Notifications
What we email you about.
New bookingsAn email for every booking.
CancellationsWhen a client cancels or moves.
Weekly summaryEvery Monday morning.
Security
Keep the workspace locked down.
Two-step sign-inRequired for admins.
Active sessions2 devices signed in.
"use client";
import { useRef, useState } from "react";
import { Bell, CreditCard, Settings, Shield, Users, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
type Section = { id: string; label: string; icon: LucideIcon; content: React.ReactNode };
const sections: Section[] = [
{ id: "general", label: "General", icon: Settings, content: <p>How your workspace appears to clients.</p> },
{ id: "members", label: "Members", icon: Users, content: <p>3 of 5 seats used.</p> },
{ id: "billing", label: "Billing", icon: CreditCard, content: <p>Team plan, billed monthly.</p> },
{ id: "notifications", label: "Notifications", icon: Bell, content: <p>What we email you about.</p> },
{ id: "security", label: "Security", icon: Shield, content: <p>Keep the workspace locked down.</p> },
];
export function VerticalTabs() {
const [active, setActive] = useState(0);
const refs = useRef<(HTMLButtonElement | null)[]>([]);
// Up/Down on wide screens, Left/Right when the list turns into a row.
const onKeyDown = (e: React.KeyboardEvent) => {
const last = sections.length - 1;
const prev = active === 0 ? last : active - 1;
const next = active === last ? 0 : active + 1;
const keys: Record<string, number> = { ArrowDown: next, ArrowRight: next, ArrowUp: prev, ArrowLeft: prev, Home: 0, End: last };
const to = keys[e.key];
if (to === undefined) return;
e.preventDefault();
setActive(to);
refs.current[to]?.focus();
};
return (
<div className="@container font-sans">
<div className="grid overflow-hidden rounded-lg border border-border bg-bg @[460px]:min-h-[300px] @[460px]:grid-cols-[172px_minmax(0,1fr)]">
<div className="grid content-start gap-1.5 border-b border-border bg-surface p-2 @[460px]:border-b-0 @[460px]:border-r">
<p className="hidden px-2.5 pt-1.5 text-[11.5px] font-medium uppercase tracking-wider text-fg-subtle @[460px]:block">Workspace</p>
<div
role="tablist"
aria-label="Settings"
aria-orientation="vertical"
className="flex gap-0.5 overflow-x-auto [scrollbar-width:none] @[460px]:flex-col @[460px]:overflow-visible"
>
{sections.map(({ id, label, icon: Icon }, i) => {
const selected = active === i;
return (
<button
key={id}
ref={(el) => { refs.current[i] = el; }}
id={"settings-tab-" + id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"settings-panel-" + id}
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
onKeyDown={onKeyDown}
className={cn(
"flex h-[34px] shrink-0 items-center gap-2.5 whitespace-nowrap rounded px-2.5 text-left text-[13.5px] font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
selected ? "bg-bg text-fg shadow-sm ring-1 ring-border [&_svg]:text-primary" : "text-fg-muted hover:bg-surface-2 hover:text-fg",
)}
>
<Icon className="size-4 shrink-0" aria-hidden />
{label}
</button>
);
})}
</div>
</div>
{sections.map((s, i) => (
<div
key={s.id}
id={"settings-panel-" + s.id}
role="tabpanel"
aria-labelledby={"settings-tab-" + s.id}
hidden={active !== i}
tabIndex={0}
className="p-5 text-sm text-fg-muted @[460px]:px-6 @[460px]:py-[22px]"
>
<h2 className="text-[15px] font-semibold text-fg">{s.label}</h2>
<div className="mt-1">{s.content}</div>
</div>
))}
</div>
</div>
);
}
// Uses the Tailwind container-queries plugin (@tailwindcss/container-queries, built into v4).
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.