Tabs
ComponentsReactTailwindTabbed content switcher with underline, pill, and bordered variants. Icons + badges, ARIA tabs pattern, arrow-key + Home/End keyboard navigation.
Underline (Default)
Production-grade starter kit.
Authentication, error boundaries, observability hooks, and rendering strategy built in from day one.
Arrow keys navigate, Home/End jump to first/last. Active tab gets primary underline.
Pill Variant
Manage your name, avatar, and public profile information.
Bordered (Browser-Style)
Production-grade starter kit.
Authentication, error boundaries, observability hooks, and rendering strategy built in from day one.
1 "use client"; 2 3 import { useState } from "react"; 4 import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; 5 import { 6 faCode, 7 faBolt, 8 faShieldHalved, 9 faKey, 10 faChartLine, 11 } from "@fortawesome/free-solid-svg-icons"; 12 import type { IconDefinition } from "@fortawesome/fontawesome-svg-core"; 13 14 type Tab = { 15 id: string; 16 label: string; 17 icon?: IconDefinition; 18 badge?: string; 19 content: React.ReactNode; 20 }; 21 22 type TabsProps = { 23 tabs: Tab[]; 24 variant?: "underline" | "pill" | "bordered"; 25 defaultId?: string; 26 }; 27 28 export function Tabs({ tabs, variant = "underline", defaultId }: TabsProps) { 29 const [activeId, setActiveId] = useState(defaultId ?? tabs[0]?.id); 30 const active = tabs.find((t) => t.id === activeId) ?? tabs[0]; 31 32 // Keyboard navigation: arrow keys + Home/End 33 function onKey(e: React.KeyboardEvent, index: number) { 34 if (e.key === "ArrowRight") { 35 e.preventDefault(); 36 setActiveId(tabs[(index + 1) % tabs.length].id); 37 } else if (e.key === "ArrowLeft") { 38 e.preventDefault(); 39 setActiveId(tabs[(index - 1 + tabs.length) % tabs.length].id); 40 } else if (e.key === "Home") { 41 e.preventDefault(); 42 setActiveId(tabs[0].id); 43 } else if (e.key === "End") { 44 e.preventDefault(); 45 setActiveId(tabs[tabs.length - 1].id); 46 } 47 } 48 49 if (variant === "pill") { 50 return ( 51 <div> 52 <div role="tablist" className="bg-muted/40 inline-flex gap-1 rounded-lg p-1"> 53 {tabs.map((tab, i) => { 54 const isActive = tab.id === activeId; 55 return ( 56 <button 57 key={tab.id} 58 type="button" 59 role="tab" 60 aria-selected={isActive} 61 aria-controls={`panel-${tab.id}`} 62 id={`tab-${tab.id}`} 63 tabIndex={isActive ? 0 : -1} 64 onKeyDown={(e) => onKey(e, i)} 65 onClick={() => setActiveId(tab.id)} 66 className={`no-hover-glow inline-flex items-center gap-2 rounded-md px-3 py-1.5 text-sm font-medium transition-colors ${ 67 isActive ? "bg-card text-foreground shadow-sm" : "text-muted-foreground hover:text-foreground" 68 }`} 69 > 70 {tab.icon && <FontAwesomeIcon icon={tab.icon} className="h-3.5 w-3.5" />} 71 {tab.label} 72 {tab.badge && ( 73 <span className={`rounded-full px-1.5 text-[10px] font-bold ${ 74 isActive ? "bg-primary text-primary-foreground" : "bg-muted text-foreground" 75 }`}> 76 {tab.badge} 77 </span> 78 )} 79 </button> 80 ); 81 })} 82 </div> 83 <div 84 role="tabpanel" 85 id={`panel-${active.id}`} 86 aria-labelledby={`tab-${active.id}`} 87 className="mt-4 rounded-lg border p-5" 88 > 89 {active.content} 90 </div> 91 </div> 92 ); 93 } 94 95 if (variant === "bordered") { 96 return ( 97 <div> 98 <div role="tablist" className="-mb-px flex flex-wrap gap-0"> 99 {tabs.map((tab, i) => { 100 const isActive = tab.id === activeId; 101 return ( 102 <button 103 key={tab.id} 104 type="button" 105 role="tab" 106 aria-selected={isActive} 107 aria-controls={`panel-${tab.id}`} 108 id={`tab-${tab.id}`} 109 tabIndex={isActive ? 0 : -1} 110 onKeyDown={(e) => onKey(e, i)} 111 onClick={() => setActiveId(tab.id)} 112 className={`no-hover-glow inline-flex items-center gap-2 rounded-t-md border border-b-0 px-4 py-2 text-sm font-medium transition-colors ${ 113 isActive ? "bg-card text-foreground border-border" : "text-muted-foreground hover:text-foreground border-transparent" 114 }`} 115 > 116 {tab.icon && <FontAwesomeIcon icon={tab.icon} className="h-3.5 w-3.5" />} 117 {tab.label} 118 </button> 119 ); 120 })} 121 </div> 122 <div 123 role="tabpanel" 124 id={`panel-${active.id}`} 125 aria-labelledby={`tab-${active.id}`} 126 className="bg-card border-border rounded-tr-lg rounded-b-lg border p-5" 127 > 128 {active.content} 129 </div> 130 </div> 131 ); 132 } 133 134 return ( 135 <div> 136 <div role="tablist" className="border-border flex gap-1 border-b"> 137 {tabs.map((tab, i) => { 138 const isActive = tab.id === activeId; 139 return ( 140 <button 141 key={tab.id} 142 type="button" 143 role="tab" 144 aria-selected={isActive} 145 aria-controls={`panel-${tab.id}`} 146 id={`tab-${tab.id}`} 147 tabIndex={isActive ? 0 : -1} 148 onKeyDown={(e) => onKey(e, i)} 149 onClick={() => setActiveId(tab.id)} 150 className={`no-hover-glow -mb-px inline-flex items-center gap-2 border-b-2 px-3 py-2 text-sm font-medium transition-colors ${ 151 isActive ? "border-primary text-foreground" : "border-transparent text-muted-foreground hover:text-foreground" 152 }`} 153 > 154 {tab.icon && <FontAwesomeIcon icon={tab.icon} className="h-3.5 w-3.5" />} 155 {tab.label} 156 {tab.badge && <span className="bg-primary/15 text-primary rounded-full px-1.5 text-[10px] font-bold">{tab.badge}</span>} 157 </button> 158 ); 159 })} 160 </div> 161 <div 162 role="tabpanel" 163 id={`panel-${active.id}`} 164 aria-labelledby={`tab-${active.id}`} 165 className="mt-4" 166 > 167 {active.content} 168 </div> 169 </div> 170 ); 171 } 172 173 // Usage — icons are optional Font Awesome IconDefinitions 174 const tabs: Tab[] = [ 175 { id: "overview", label: "Overview", icon: faCode, content: <p>Overview content</p> }, 176 { id: "performance", label: "Performance", icon: faBolt, badge: "98", content: <p>Perf stats</p> }, 177 { id: "security", label: "Security", icon: faShieldHalved, content: <p>Security details</p> }, 178 ]; 179 180 const settingsTabs: Tab[] = [ 181 { id: "profile", label: "Profile", content: <p>Profile settings</p> }, 182 { id: "security", label: "Security", icon: faKey, content: <p>Password & 2FA</p> }, 183 { id: "billing", label: "Billing", badge: "Pro", content: <p>Plan & invoices</p> }, 184 { id: "analytics", label: "Analytics", icon: faChartLine, content: <p>Usage metrics</p> }, 185 ]; 186 187 <Tabs tabs={tabs} variant="underline" /> 188 <Tabs tabs={settingsTabs} variant="pill" /> 189 <Tabs tabs={tabs} variant="bordered" />