/Navigation
Navigation • React
Expanding Icon Tabs
An icon bar where the chosen tab widens to reveal its label and the rest of the row slides over to make room.
Installation
Requires lucide-react for its icons. The shadcn CLI installs it for you; copying the code by hand means installing it yourself.
Terminal
npx shadcn@latest add https://microkit.co/r/expanding-icon-tabs.jsonCode
1"use client";23import { useEffect, useRef, useState } from "react";4import type { KeyboardEvent as ReactKeyboardEvent } from "react";5import { Activity, Compass, CreditCard, SlidersHorizontal, Users } from "lucide-react";6import type { LucideIcon } from "lucide-react";78type TabItem = { id: string; label: string; icon: LucideIcon } | { separator: true };910const items: TabItem[] = [11{ id: "overview", label: "Overview", icon: Compass },12{ id: "activity", label: "Activity", icon: Activity },13{ separator: true },14{ id: "filters", label: "Filters", icon: SlidersHorizontal },15{ id: "members", label: "Members", icon: Users },16{ id: "billing", label: "Billing", icon: CreditCard },17];1819export function ExpandingIconTabs() {20const [active, setActive] = useState<string | null>(null);21const rootRef = useRef<HTMLDivElement>(null);2223useEffect(() => {24if (!active) return;25const region = rootRef.current?.parentElement;26if (!region) return;27const handlePointerDown = (event: Event) => {28if (!rootRef.current?.contains(event.target as Node)) setActive(null);29};30region.addEventListener("pointerdown", handlePointerDown);31return () => region.removeEventListener("pointerdown", handlePointerDown);32}, [active]);3334const moveFocus = (delta: number) => {35const tabs = Array.from(rootRef.current?.querySelectorAll<HTMLButtonElement>("[data-tab]") ?? []);36if (!tabs.length) return;37const index = tabs.indexOf(document.activeElement as HTMLButtonElement);38const next = index < 0 ? 0 : (index + delta + tabs.length) % tabs.length;39tabs[next].focus({ preventScroll: true });40};4142const handleKeyDown = (event: ReactKeyboardEvent<HTMLDivElement>) => {43if (event.key === "ArrowRight" || event.key === "ArrowLeft") {44event.preventDefault();45moveFocus(event.key === "ArrowRight" ? 1 : -1);46return;47}48if (event.key === "Escape" && active) {49event.preventDefault();50setActive(null);51}52};5354return (55<div56className="flex max-w-full flex-wrap items-center gap-1 rounded-xl border border-[#2b2f36] bg-[#121418] p-1 text-[11px] text-[#dfe2e5] shadow-[inset_0_1px_0_rgba(255,255,255,.04),0_10px_24px_rgba(0,0,0,.35)]"57role="toolbar"58aria-label="Workspace views"59ref={rootRef}60onKeyDown={handleKeyDown}61>62{items.map((item, index) =>63"separator" in item ? (64<span65key={`separator-${index}`}66className="mx-[2px] h-[18px] w-px flex-none bg-[#23262c]"67role="separator"68aria-orientation="vertical"69/>70) : (71<button72key={item.id}73type="button"74className={`inline-flex h-[30px] items-center rounded-[9px] border-0 text-[11px] [transition:gap_.5s_cubic-bezier(.32,.72,0,1),padding_.5s_cubic-bezier(.32,.72,0,1),background-color_.3s_ease,color_.3s_ease] motion-reduce:[transition-duration:.01ms] focus-visible:outline focus-visible:outline-[1.5px] focus-visible:outline-offset-[-1.5px] focus-visible:outline-[#f97316] ${75active === item.id76? "gap-1.5 bg-[#22262c] px-[11px] text-[#f4f5f7]"77: "gap-0 bg-transparent px-2 text-[#868d97] hover:bg-[#1b1e23] hover:text-[#dfe2e5]"78}`}79data-tab=""80aria-label={item.label}81aria-pressed={active === item.id}82onClick={() => setActive(active === item.id ? null : item.id)}83>84<span85className={`grid flex-none place-items-center [transition:color_.3s_ease] motion-reduce:[transition-duration:.01ms] ${86active === item.id ? "text-[#f97316]" : ""87}`}88>89<item.icon size={15} strokeWidth={1.8} aria-hidden="true" />90</span>91<span92className={`grid overflow-hidden [transition:grid-template-columns_.5s_cubic-bezier(.32,.72,0,1),opacity_.3s_ease] motion-reduce:[transition-duration:.01ms] ${93active === item.id ? "grid-cols-[1fr] opacity-100" : "grid-cols-[0fr] opacity-0"94}`}95aria-hidden="true"96>97<span className="min-w-0 overflow-hidden whitespace-nowrap">{item.label}</span>98</span>99</button>100),101)}102</div>103);104}