Overview
Tabs switch sections without a page reload. aSaaSin wraps Radix Tabs and adds small UX helpers for overflowed lists and direction-aware animations. Colors and focus rings follow your theme tokens. See shadcn/ui Tabs for the base API.
Exports
From @/components/ui/Tabs
:
Tabs
,TabsList
,TabsTrigger
,TabsContent
TabsList
acceptsactiveTab
to center the selected tab- Helpers in the same folder:
useCenterActiveTab(ref, activeTab)
- keeps the active tab centereduseCheckScroll(ref)
- tells if you can scroll left/right to show gradient hintsuseTabDirection(tabs, defaultTab)
- returns{ activeTab, direction, handleTabChange }
Basic usage
Uncontrolled tabs with a scrollable list. Keep labels short. Use icons with text for clarity.
'use client';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
export function BasicTabs() {
return (
<Tabs defaultValue="tab-1">
<TabsList>
<TabsTrigger value="tab-1">Overview</TabsTrigger>
<TabsTrigger value="tab-2">Details</TabsTrigger>
<TabsTrigger value="tab-3">Usage</TabsTrigger>
</TabsList>
<TabsContent value="tab-1">Overview content…</TabsContent>
<TabsContent value="tab-2">Details content…</TabsContent>
<TabsContent value="tab-3">Usage content…</TabsContent>
</Tabs>
);
}
Center active & overflow hints
TabsList
centers the selected tab and shows gradient edges when the list overflows. Pass activeTab
to keep it centered as the value changes.
'use client';
import * as React from 'react';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
const items = [
{ value: 'overview', label: 'Overview' },
{ value: 'details', label: 'Details' },
{ value: 'usage', label: 'Usage' },
{ value: 'api', label: 'API' },
{ value: 'faq', label: 'FAQ' },
];
export function CenteredTabs() {
const [value, setValue] = React.useState('overview');
return (
<Tabs value={value} onValueChange={setValue}>
<TabsList activeTab={value}>
{items.map((t) => (
<TabsTrigger key={t.value} value={t.value}>
{t.label}
</TabsTrigger>
))}
</TabsList>
{items.map((t) => (
<TabsContent key={t.value} value={t.value}>
{t.label} content…
</TabsContent>
))}
</Tabs>
);
}
Direction-aware animations
For nicer transitions, use useTabDirection
to detect whether the user moved left or right and switch animation classes accordingly.
'use client';
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/Tabs';
import { useTabDirection } from '@/components/ui/Tabs/useTabDirection';
import { cn } from '@/utils';
const tabs = [
{ value: 'one', label: 'One' },
{ value: 'two', label: 'Two' },
{ value: 'three', label: 'Three' },
];
export function AnimatedTabs() {
const { activeTab, direction, handleTabChange } = useTabDirection(tabs, 'one');
return (
<Tabs value={activeTab} onValueChange={handleTabChange}>
<TabsList activeTab={activeTab}>
{tabs.map((t) => (
<TabsTrigger key={t.value} value={t.value}>
{t.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((t) => (
<TabsContent
key={t.value}
value={t.value}
className={cn(
'data-[state=active]:animate-in data-[state=active]:duration-500',
direction === 'left'
? 'data-[state=active]:slide-in-from-right'
: 'data-[state=active]:slide-in-from-left',
)}
>
{t.label} content…
</TabsContent>
))}
</Tabs>
);
}
Styling notes
- Token-based colors, dark mode supported out of the box.
- Scrollbar is hidden with
scrollbar-hide
; list remains scrollable. - Triggers use Radix
data-[state=active]
for active state styles. - Keep focus rings visible on
TabsTrigger
.
Prefer 2–6 tabs per group. If tab changes impact routing, sync value
with a query param. Load heavy content on first reveal and cache it.