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 accepts activeTab to center the selected tab
  • Helpers in the same folder:
    • useCenterActiveTab(ref, activeTab) - keeps the active tab centered
    • useCheckScroll(ref) - tells if you can scroll left/right to show gradient hints
    • useTabDirection(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.