All Taste components
Tabs
Full-width segmented tabs
Equal-width segments that fill their container, with a badge on one option and the content switching below.
neutral · brand-filled · badge in a segment · 2 and 3 segments
€24per seat / month
Cancel any time.
€228 billed once a year.
"use client";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";
type Option = { id: string; label: string; badge?: string };
export function SegmentedTabs({ options, value, onChange, label, tone = "neutral" }: {
options: Option[];
value: string;
onChange: (id: string) => void;
label: string;
tone?: "neutral" | "brand";
}) {
const refs = useRef<(HTMLButtonElement | null)[]>([]);
const index = Math.max(0, options.findIndex((o) => o.id === value));
const onKeyDown = (e: React.KeyboardEvent) => {
const last = options.length - 1;
const keys: Record<string, number> = {
ArrowRight: index === last ? 0 : index + 1,
ArrowLeft: index === 0 ? last : index - 1,
Home: 0,
End: last,
};
const next = keys[e.key];
if (next === undefined) return;
e.preventDefault();
onChange(options[next].id);
refs.current[next]?.focus();
};
return (
<div role="tablist" aria-label={label} className="grid auto-cols-fr grid-flow-col gap-1 rounded-lg bg-surface-2 p-1 font-sans">
{options.map((o, i) => {
const selected = i === index;
return (
<button
key={o.id}
ref={(el) => { refs.current[i] = el; }}
id={"seg-tab-" + o.id}
type="button"
role="tab"
aria-selected={selected}
aria-controls={"seg-panel-" + o.id}
tabIndex={selected ? 0 : -1}
onClick={() => onChange(o.id)}
onKeyDown={onKeyDown}
className={cn(
"inline-flex h-9 min-w-0 items-center justify-center gap-1.5 whitespace-nowrap rounded px-2 text-[13.5px] font-medium transition",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
!selected && "text-fg-muted hover:text-fg",
selected && tone === "neutral" && "bg-bg text-fg shadow-sm ring-1 ring-border",
selected && tone === "brand" && "bg-primary text-primary-fg",
)}
>
{o.label}
{o.badge && <span className="rounded-full bg-primary-soft px-1.5 py-0.5 text-[11px] text-primary-soft-fg">{o.badge}</span>}
</button>
);
})}
</div>
);
}
// Example: the price below follows the chosen period.
export function BillingPeriod() {
const [period, setPeriod] = useState("monthly");
return (
<div className="grid max-w-[340px] gap-3.5">
<SegmentedTabs
label="Billing period"
value={period}
onChange={setPeriod}
options={[{ id: "monthly", label: "Monthly" }, { id: "yearly", label: "Yearly", badge: "−20%" }]}
/>
<div id={"seg-panel-" + period} role="tabpanel" aria-labelledby={"seg-tab-" + period} tabIndex={0}>
<p className="text-[34px] font-semibold tracking-tight text-fg">
{period === "monthly" ? "€24" : "€19"} <span className="text-sm font-normal text-fg-subtle">per seat / month</span>
</p>
</div>
</div>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.