All Taste components
Tabs
Pill tabs
Rounded filter tabs with counts that switch the list underneath, in a solid and a soft style.
solid · soft · counts · switching panels
- Product designerRemote · Full timeDesign
- Senior frontend engineerLisbon or remoteEngineering
- Support leadDublin · Full timeSupport
- Product designerRemote · Full timeDesign
- Brand designerLisbon · ContractDesign
- Senior frontend engineerLisbon or remoteEngineering
- Backend engineerRemote · Full timeEngineering
- Mobile engineerBerlin · Full timeEngineering
- Support leadDublin · Full timeSupport
Soft
"use client";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";
type Tab = { id: string; label: string; count?: number; content: React.ReactNode };
export function PillTabs({ tabs, variant = "solid", label }:
{ tabs: Tab[]; variant?: "solid" | "soft"; 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="grid gap-3.5 font-sans">
<div role="tablist" aria-label={label} className="flex flex-wrap gap-1.5">
{tabs.map((tab, i) => {
const selected = active === i;
return (
<button
key={tab.id}
ref={(el) => { refs.current[i] = el; }}
id={"pill-tab-" + tab.id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"pill-panel-" + tab.id}
tabIndex={selected ? 0 : -1}
onClick={() => setActive(i)}
onKeyDown={onKeyDown}
className={cn(
"inline-flex h-8 items-center gap-1.5 whitespace-nowrap rounded-full border px-3.5 text-[13px] font-medium transition-colors",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
variant === "solid"
? selected
? "border-fg bg-fg text-bg"
: "border-border bg-bg text-fg-muted hover:border-border-strong hover:text-fg"
: selected
? "border-transparent bg-primary-soft text-primary-soft-fg"
: "border-transparent text-fg-muted hover:bg-surface-2 hover:text-fg",
)}
>
{tab.label}
{tab.count !== undefined && (
<span className={cn("text-[11.5px] tabular-nums", selected ? "opacity-70" : "text-fg-subtle")}>{tab.count}</span>
)}
</button>
);
})}
</div>
{tabs.map((tab, i) => (
<div
key={tab.id}
id={"pill-panel-" + tab.id}
role="tabpanel"
aria-labelledby={"pill-tab-" + tab.id}
hidden={active !== i}
tabIndex={0}
className="focus-visible:outline-none"
>
{tab.content}
</div>
))}
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.