Animated Tabs
Tabs on Radix with an underline or pill indicator that slides to the active tab; keyboard and screen-reader ready.
"use client";
import { useState, type CSSProperties } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/registry/ward/ui/tabs";
import "./styles/animated-tabs.css";
const tabs = [{ label: "Overview", content: "A clear summary keeps the next step visible." }, { label: "Activity", content: "Recent changes stay close to the work." }, { label: "Details", content: "Supporting information appears when needed." }];
export default function AnimatedTabs({ variant = "Underline" }: { variant?: string }) {
const [active, setActive] = useState(tabs[0].label);
const pill = variant === "Pill";
const style = { "--active": tabs.findIndex(tab => tab.label === active), "--count": tabs.length } as CSSProperties;
return <Tabs value={active} onValueChange={setActive} className="ward-tabs" data-variant={pill ? "pill" : "underline"}>
<TabsList variant={pill ? "default" : "line"} className="ward-tabs-list" style={style} aria-label="Example sections">{tabs.map(tab => <TabsTrigger key={tab.label} value={tab.label}>{tab.label}</TabsTrigger>)}</TabsList>
{tabs.map((tab, index) => <TabsContent key={tab.label} value={tab.label} className="ward-tabs-panel"><span>0{index + 1} / 0{tabs.length}</span><h3>{tab.label}</h3><p>{tab.content}</p></TabsContent>)}
</Tabs>;
}.ward-tabs { width: min(100%, 560px); margin: auto; color: var(--foreground); }
.ward-tabs-list { position: relative; display: grid; grid-template-columns: repeat(var(--count), minmax(0, 1fr)); width: 100%; }
.ward-tabs-list::before { position: absolute; left: 0; width: calc(100% / var(--count)); content: ""; transform: translateX(calc(var(--active) * 100%)); transition: transform 200ms cubic-bezier(.2, 0, 0, 1); }
.ward-tabs[data-variant="underline"] .ward-tabs-list { border-bottom: 1px solid var(--border); }
.ward-tabs[data-variant="underline"] .ward-tabs-list::before { bottom: -1px; height: 2px; border-radius: 2px; background: var(--foreground); }
.ward-tabs[data-variant="pill"] .ward-tabs-list { height: 36px; padding: 4px; border-radius: 999px; background: var(--secondary); }
.ward-tabs[data-variant="pill"] .ward-tabs-list::before { top: 4px; bottom: 4px; left: 4px; width: calc((100% - 8px) / var(--count)); border-radius: 999px; background: var(--background); box-shadow: 0 0 0 1px var(--border); }
.ward-tabs [data-slot="tabs-trigger"] { position: relative; z-index: 1; height: 100%; border: 0; border-radius: 999px; background: transparent; box-shadow: none; color: var(--muted-foreground); font-size: 13px; }
.ward-tabs [data-slot="tabs-trigger"][data-state="active"] { background: transparent; box-shadow: none; color: var(--foreground); }
.ward-tabs [data-slot="tabs-trigger"]::after { display: none; }
.ward-tabs-panel { min-height: 170px; padding: 24px 4px; }
.ward-tabs-panel span { color: var(--muted-foreground); font: 11px/1.4 var(--font-mono, ui-monospace, monospace); letter-spacing: .08em; }
.ward-tabs-panel h3 { margin: 12px 0 4px; font-size: 16px; font-weight: 500; }
.ward-tabs-panel p { margin: 0; color: var(--muted-foreground); font-size: 14px; line-height: 1.6; }
@media (prefers-reduced-motion: reduce) { .ward-tabs-list::before { transition: none; } }Installation
pnpm dlx shadcn@latest add @ward/animated-tabsnpx shadcn@latest add @ward/animated-tabsyarn dlx shadcn@latest add @ward/animated-tabsbunx --bun shadcn@latest add @ward/animated-tabsAdd the Ward registry to components.json once:
"registries": {
"@ward": "https://ward.so/r/{name}.json"
}Or install from the URL with no configuration: npx shadcn@latest add https://ward.so/r/animated-tabs.json
Add the Ward items it builds on.
npx shadcn@latest add @ward/tabsCopy each file from the Code tab into:
components/ward/AnimatedTabs.tsxcomponents/ward/styles/animated-tabs.css
Update the import paths to match your project.
Usage
import AnimatedTabs from "@/components/ward/AnimatedTabs";<AnimatedTabs variant="Pill" />Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "Underline" | "Pill" | "Underline" | Style of the indicator that slides to the active tab. |
How to build
Switch between related views without losing keyboard or small-screen access.
Use tabs only when panels are peers; a page link is clearer for separate destinations. Keep the active panel in the reading order and let the list scroll on narrow screens.
Ward · Version 1.1.0 · Registry manifest