Keyboard-navigable content tabs with quiet pill or underline styling and a clear selected state in both themes.
Your workspace at a glance
24
Active files
8
Teammates
import { Activity, Files, Users } from "lucide-react";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ballmac/tabs";
export default function TabsDemo() {
return (
<Tabs
defaultValue="overview"
className="w-full max-w-sm rounded-xl border bg-card p-4 shadow-sm"
>
<TabsList aria-label="Workspace views">
<TabsTrigger value="overview">
<Activity aria-hidden="true" className="size-4" />
Overview
</TabsTrigger>
<TabsTrigger value="files">
<Files aria-hidden="true" className="size-4" />
Files
</TabsTrigger>
<TabsTrigger value="team">
<Users aria-hidden="true" className="size-4" />
Team
</TabsTrigger>
</TabsList>
<TabsContent value="overview">
<p className="text-sm font-semibold">Your workspace at a glance</p>
<div className="mt-3 grid grid-cols-2 gap-2">
<div className="rounded-lg border bg-background p-3">
<p className="text-2xl font-semibold tabular-nums">24</p>
<p className="text-xs text-muted-foreground">Active files</p>
</div>
<div className="rounded-lg border bg-background p-3">
<p className="text-2xl font-semibold tabular-nums">8</p>
<p className="text-xs text-muted-foreground">Teammates</p>
</div>
</div>
</TabsContent>
<TabsContent value="files">
<p className="text-sm font-semibold">Recent files</p>
<p className="mt-2 text-sm text-muted-foreground">
Product brief · Brand guide · Launch checklist
</p>
</TabsContent>
<TabsContent value="team">
<p className="text-sm font-semibold">People in this workspace</p>
<p className="mt-2 text-sm text-muted-foreground">
8 teammates can collaborate here.
</p>
</TabsContent>
</Tabs>
);
}Installation
$ pnpm dlx shadcn@latest add @ballmac/tabsInstall the dependencies.
$ pnpm add radix-uiAdd the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/directionCopy the source into your project.
components/ballmac/tabs.tsx// Ballmac UI: Tabs. https://ui.ballmac.com/components/tabs "use client"; import * as React from "react"; import { Tabs as Primitive } from "radix-ui"; import { useDirection } from "@/lib/ballmac/direction"; import { cn } from "@/lib/utils"; type TabsProps = React.ComponentProps<typeof Primitive.Root>; function Tabs({ className, ...props }: TabsProps) { const dir = useDirection(props.dir); return ( <Primitive.Root dir={dir} data-slot="tabs" className={cn("flex min-w-0 flex-col gap-4", className)} {...props} /> ); } type TabsListProps = React.ComponentProps<typeof Primitive.List> & { /** Visual treatment of the selected tab. */ variant?: "pills" | "underline"; }; function TabsList({ className, variant = "pills", ...props }: TabsListProps) { return ( <Primitive.List data-slot="tabs-list" data-variant={variant} className={cn( "inline-flex max-w-full items-center gap-1 self-start overflow-x-auto rounded-lg p-1 text-muted-foreground", variant === "pills" ? "bg-muted" : "rounded-none border-b bg-transparent pb-0", className, )} {...props} /> ); } type TabsTriggerProps = React.ComponentProps<typeof Primitive.Trigger>; function TabsTrigger({ className, ...props }: TabsTriggerProps) { return ( <Primitive.Trigger data-slot="tabs-trigger" className={cn( "inline-flex min-h-9 shrink-0 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium whitespace-nowrap outline-none transition-[color,background-color,box-shadow] duration-150 hover:text-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50 data-[state=active]:text-foreground [[data-variant=pills]_&]:data-[state=active]:bg-background [[data-variant=pills]_&]:data-[state=active]:shadow-xs [[data-variant=underline]_&]:rounded-none [[data-variant=underline]_&]:border-b-2 [[data-variant=underline]_&]:border-transparent [[data-variant=underline]_&]:data-[state=active]:border-primary motion-reduce:transition-none", className, )} {...props} /> ); } type TabsContentProps = React.ComponentProps<typeof Primitive.Content>; function TabsContent({ className, ...props }: TabsContentProps) { return ( <Primitive.Content data-slot="tabs-content" className={cn( "min-w-0 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50", className, )} {...props} /> ); } export { Tabs, TabsList, TabsTrigger, TabsContent, type TabsProps, type TabsListProps, type TabsTriggerProps, type TabsContentProps, };Update the import paths to match your project setup.
Usage
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ballmac/tabs"The full example is in the Code tab above.
Examples
Underline tabs
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ballmac/tabs";
export default function TabsStates() {
return (
<Tabs defaultValue="activity" className="w-full max-w-sm">
<TabsList variant="underline" aria-label="Project details">
<TabsTrigger value="activity">Activity</TabsTrigger>
<TabsTrigger value="details">Details</TabsTrigger>
<TabsTrigger value="settings">Settings</TabsTrigger>
</TabsList>
<TabsContent
value="activity"
className="pt-2 text-sm text-muted-foreground"
>
Your latest project updates appear here.
</TabsContent>
<TabsContent
value="details"
className="pt-2 text-sm text-muted-foreground"
>
Shared project information and files.
</TabsContent>
<TabsContent
value="settings"
className="pt-2 text-sm text-muted-foreground"
>
Manage access and notifications.
</TabsContent>
</Tabs>
);
}API reference
| Prop | Type | Default |
|---|---|---|
variantVisual treatment of the selected tab. | "pills" | "underline" | "pills" |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Tab | Focuses the active tab |
| Arrow keys / Home / End | Moves among tabs |
| Tab again | Moves into the active panel |
Use with AI
Switches between a small number of peer views while retaining Radix tablist semantics and roving focus. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Tabs (@ballmac/tabs) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Related panels within one page
- Compact dashboards and detail views
Not for
- Steps in a workflow; use progress-steps
- Site navigation to separate pages
Registry JSON: https://ui.ballmac.com/r/tabs.json
Credits
Free to use in personal and commercial projects.
- npm
- radix-ui
- Registry
- @ballmac/directionshadcn/utils
Pairs well with
Card
A composable content surface with compact spacing, an action slot, and optional interactive feedback.
Badge
A small pill label in four variants with an optional status dot and success, warning and error tones. Style links with badgeVariants().
Animated Tabs
Radix Tabs with a pill or underline indicator that slides between triggers on a spring, and panels that fade in. Keeps full keyboard support and ARIA; controlled or uncontrolled.
Back To Top
A floating button that appears after scrolling, draws a progress ring, scrolls smoothly (instantly under reduced motion) and moves focus to the content.