Tabs
Capsule pill tabs with pastel active tab highlights.
components/ui/tabs.tsx
import * as React from 'react'
import { motion } from 'motion/react'
import { cn } from '@/lib/utils'
export interface TabItem {
id: string
label: string
icon?: React.ReactNode
badge?: string
}
export interface TabsProps {
tabs: TabItem[]
activeTab?: string
defaultTab?: string
onTabChange?: (id: string) => void
variant?: 'pill' | 'underline' | 'boxed'
className?: string
}
export function Tabs({
tabs,
activeTab: controlledActive,
defaultTab,
onTabChange,
variant = 'pill',
className,
}: TabsProps) {
const [internalActive, setInternalActive] = React.useState(defaultTab || tabs[0]?.id)
const currentTab = controlledActive !== undefined ? controlledActive : internalActive
const handleSelect = (id: string) => {
setInternalActive(id)
onTabChange?.(id)
}
if (variant === 'pill') {
return (
<div
className={cn(
'inline-flex items-center gap-1.5 p-1.5 rounded-full bg-rk-ink text-rk-canvas rk-border rk-shadow-md relative',
className
)}
>
{tabs.map((tab) => {
const isActive = currentTab === tab.id
return (
<button
key={tab.id}
onClick={() => handleSelect(tab.id)}
className={cn(
'relative flex items-center gap-2 px-5 py-2 rounded-full font-extrabold text-xs tracking-wide transition-colors z-10 select-none cursor-pointer',
isActive ? 'text-rk-ink' : 'text-white/80 hover:text-white'
)}
>
{isActive && (
<motion.div
layoutId="activeTabPill"
transition={{ type: 'spring', stiffness: 500, damping: 30 }}
className="absolute inset-0 bg-rk-primary rounded-full rk-border-sm rk-shadow-sm -z-10"
/>
)}
{tab.icon}
<span>{tab.label}</span>
{tab.badge && (
<span
className={cn(
'text-[10px] font-mono px-1.5 py-0.2 rounded-full font-black',
isActive ? 'bg-black text-white' : 'bg-white/20 text-white'
)}
>
{tab.badge}
</span>
)}
</button>
)
})}
</div>
)
}
return (
<div className={cn('flex items-center gap-2 border-b-3 border-rk-ink pb-1 relative', className)}>
{tabs.map((tab) => {
const isActive = currentTab === tab.id
return (
<button
key={tab.id}
onClick={() => handleSelect(tab.id)}
className={cn(
'relative flex items-center gap-2 px-4 py-2 font-display font-extrabold text-sm transition-all rounded-t-xl rk-border-sm border-b-0 cursor-pointer',
isActive
? 'bg-rk-primary text-rk-ink rk-shadow-sm -translate-y-1'
: 'bg-rk-surface text-rk-ink/70 hover:bg-rk-canvas'
)}
>
{tab.icon}
<span>{tab.label}</span>
</button>
)
})}
</div>
)
}
- Visual style
- pastel
DependenciesBundle weight · light
clsxtailwind-merge