Skip to content
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.