All Taste components
Tabs
Tabs with icons
Tabs that pair each label with an icon, as an underlined row or a compact bar with the icon on top.
icon + label underline · icon-on-top bar · switching panels
This week at a glance
Bookings are up on last week and two invoices are waiting.
- Bookings
- 148
- Revenue
- €9,420
- Due
- 2
Where visitors come from
Most bookings start on your website, then social links.
- Website
- 61%
- Social
- 27%
- Direct
- 12%
Your regulars
Customers who booked three times or more this quarter.
- Regulars
- 84
- New
- 36
- Lapsed
- 9
Workspace settings
Name, time zone, and who can see revenue.
- Members
- 5
- Time zone
- GMT+1
- Plan
- Team
"use client";
import { useRef, useState } from "react";
import { BarChart3, Home, Settings, Users, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
type Tab = { id: string; label: string; icon: LucideIcon; content: React.ReactNode };
const tabs: Tab[] = [
{ id: "overview", label: "Overview", icon: Home, content: <p>This week at a glance.</p> },
{ id: "analytics", label: "Analytics", icon: BarChart3, content: <p>Where visitors come from.</p> },
{ id: "customers", label: "Customers", icon: Users, content: <p>Your regulars.</p> },
{ id: "settings", label: "Settings", icon: Settings, content: <p>Workspace settings.</p> },
];
export function IconTabs({ layout = "inline" }: { layout?: "inline" | "stacked" }) {
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.5 font-sans">
<div
role="tablist"
aria-label="Workspace"
className={cn(
layout === "inline"
? "flex gap-5 overflow-x-auto shadow-[inset_0_-1px_0] shadow-border [scrollbar-width:none]"
: "grid auto-cols-fr grid-flow-col gap-1 rounded-lg border border-border bg-bg p-1",
)}
>
{tabs.map(({ id, label, icon: Icon }, i) => {
const selected = active === i;
return (
<button
key={id}
ref={(el) => { refs.current[i] = el; }}
id={"icon-tab-" + id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"icon-panel-" + id}
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
onKeyDown={onKeyDown}
className={cn(
"font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
layout === "inline"
? cn(
"relative inline-flex h-[42px] shrink-0 items-center gap-2 whitespace-nowrap text-[13.5px]",
"after:absolute after:inset-x-0 after:bottom-0 after:h-0.5 after:rounded-t-sm",
selected ? "text-fg after:bg-primary [&_svg]:text-primary" : "text-fg-muted hover:text-fg",
)
: cn(
"grid min-w-0 justify-items-center gap-1 rounded px-1 pb-2 pt-2.5 text-xs",
selected ? "bg-primary-soft text-primary-soft-fg" : "text-fg-muted hover:bg-surface hover:text-fg",
),
)}
>
<Icon className={layout === "inline" ? "size-4" : "size-[18px]"} aria-hidden />
{label}
</button>
);
})}
</div>
{tabs.map((tab, i) => (
<div key={tab.id} id={"icon-panel-" + tab.id} role="tabpanel" aria-labelledby={"icon-tab-" + tab.id} hidden={active !== i} tabIndex={0} className="text-sm text-fg-muted">
{tab.content}
</div>
))}
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.