Tabs
A set of layered sections of content that display one panel at a time.
The Tabs component provides a way to organize content into multiple panels, with only one panel visible at a time. Users can switch between panels by clicking on the corresponding tab trigger. Built from scratch using React and native HTML elements. No dependencies on any UI library.
Code
TypeScript: Copy this code into components/ui/tabs.tsx:
tsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
interface TabsContextValue {
value: string
onValueChange: (value: string) => void
}
const TabsContext = React.createContext<TabsContextValue | undefined>(undefined)
const useTabs = () => {
const context = React.useContext(TabsContext)
if (!context) {
throw new Error("Tabs components must be used within Tabs")
}
return context
}
interface TabsProps {
defaultValue?: string
value?: string
onValueChange?: (value: string) => void
children: React.ReactNode
className?: string
}
const Tabs = React.forwardRef<HTMLDivElement, TabsProps>(
({ defaultValue, value: controlledValue, onValueChange, children, className, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState<string>(
defaultValue || ""
)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue: string) => {
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
}, [isControlled, onValueChange])
return (
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
<div ref={ref} className={cn("w-full", className)} {...props}>
{children}
</div>
</TabsContext.Provider>
)
}
)
Tabs.displayName = "Tabs"
const TabsList = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md border-2 border-foreground bg-muted p-1 neobrutalism-shadow",
className
)}
{...props}
/>
))
TabsList.displayName = "TabsList"
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string
}
const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
({ className, value, children, ...props }, ref) => {
const { value: selectedValue, onValueChange } = useTabs()
const isActive = selectedValue === value
return (
<button
ref={ref}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => onValueChange(value)}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-bold transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive
? "bg-primary text-primary-foreground border-2 border-foreground neobrutalism-shadow-sm"
: "text-muted-foreground hover:bg-background hover:text-foreground",
className
)}
{...props}
>
{children}
</button>
)
}
)
TabsTrigger.displayName = "TabsTrigger"
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string
}
const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
({ className, value, children, ...props }, ref) => {
const { value: selectedValue } = useTabs()
const isActive = selectedValue === value
if (!isActive) return null
return (
<div
ref={ref}
role="tabpanel"
className={cn(
"mt-2 focus-visible:outline-none",
className
)}
{...props}
>
{children}
</div>
)
}
)
TabsContent.displayName = "TabsContent"
export { Tabs, TabsList, TabsTrigger, TabsContent }JavaScript: Copy this code into components/ui/tabs.jsx:
jsx
"use client"
import * as React from "react"
import { cn } from "@/lib/utils"
const TabsContext = React.createContext(undefined)
const useTabs = () => {
const context = React.useContext(TabsContext)
if (!context) {
throw new Error("Tabs components must be used within Tabs")
}
return context
}
const Tabs = React.forwardRef(
({ defaultValue, value: controlledValue, onValueChange, children, className, ...props }, ref) => {
const [uncontrolledValue, setUncontrolledValue] = React.useState(
defaultValue || ""
)
const isControlled = controlledValue !== undefined
const value = isControlled ? controlledValue : uncontrolledValue
const handleValueChange = React.useCallback((newValue) => {
if (!isControlled) {
setUncontrolledValue(newValue)
}
onValueChange?.(newValue)
}, [isControlled, onValueChange])
return (
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
<div ref={ref} className={cn("w-full", className)} {...props}>
{children}
</div>
</TabsContext.Provider>
)
}
)
Tabs.displayName = "Tabs"
const TabsList = React.forwardRef(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md border-2 border-foreground bg-muted p-1 neobrutalism-shadow",
className
)}
{...props}
/>
)
)
TabsList.displayName = "TabsList"
const TabsTrigger = React.forwardRef(
({ className, value, children, ...props }, ref) => {
const { value: selectedValue, onValueChange } = useTabs()
const isActive = selectedValue === value
return (
<button
ref={ref}
type="button"
role="tab"
aria-selected={isActive}
onClick={() => onValueChange(value)}
className={cn(
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-bold transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
isActive
? "bg-primary text-primary-foreground border-2 border-foreground neobrutalism-shadow-sm"
: "text-muted-foreground hover:bg-background hover:text-foreground",
className
)}
{...props}
>
{children}
</button>
)
}
)
TabsTrigger.displayName = "TabsTrigger"
const TabsContent = React.forwardRef(
({ className, value, children, ...props }, ref) => {
const { value: selectedValue } = useTabs()
const isActive = selectedValue === value
if (!isActive) return null
return (
<div
ref={ref}
role="tabpanel"
className={cn(
"mt-2 focus-visible:outline-none",
className
)}
{...props}
>
{children}
</div>
)
}
)
TabsContent.displayName = "TabsContent"
export { Tabs, TabsList, TabsTrigger, TabsContent }Usage
TypeScript:
tsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
function MyComponent() {
return (
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">
<div className="space-y-4">
<h3 className="text-lg font-bold">Account</h3>
<p>Configure your Things component library settings here.</p>
</div>
</TabsContent>
<TabsContent value="password">
<div className="space-y-4">
<h3 className="text-lg font-bold">Password</h3>
<p>Change your password here.</p>
</div>
</TabsContent>
</Tabs>
)
}JavaScript:
jsx
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
function MyComponent() {
return (
<Tabs defaultValue="account">
<TabsList>
<TabsTrigger value="account">Account</TabsTrigger>
<TabsTrigger value="password">Password</TabsTrigger>
</TabsList>
<TabsContent value="account">
<div className="space-y-4">
<h3 className="text-lg font-bold">Account</h3>
<p>Configure your Things component library settings here.</p>
</div>
</TabsContent>
<TabsContent value="password">
<div className="space-y-4">
<h3 className="text-lg font-bold">Password</h3>
<p>Change your password here.</p>
</div>
</TabsContent>
</Tabs>
)
}Make sure you also have the lib/utils.ts file with the cn helper function.
Examples
Default
Account
Configure your Things component library settings here. Click save when you're done.