Sliding Tabs
Pill tabs whose label colour is clipped to a sliding pill, with the active icon fading in beside it.
Preview
Setup
Install with the shadcn CLI
One command adds every file, the npm packages and any Kinetik components it builds on. Needs a components.json; the CLI offers to create one if your project has none.
Or install manually
- 1
Run in your terminal:
$npm install clsx tailwind-merge @radix-ui/react-slot class-variance-authority - 2
Copy
lib/utils.tsfrom the Code tab. Skip if you already havecn(). - 3
Copy
styles/tokens.cssfrom the Code tab and import it in your global CSS after Tailwind:@import "./styles/tokens.css"; - 4
This component uses Button. Copy
components/ui/button/from the Code tab too, at the same path so the import resolves. Skip any you already have. - 5
Create a
components/ui/sliding-tabs/folder and copy every file from the Code tab into it. Each file does one job, so you can read or change a piece without the rest. - 6
Import and render:
Exampleimport { SlidingTabs } from "@/components/ui/sliding-tabs" <SlidingTabs tabs={[{ value: "one", label: "One" }, { value: "two", label: "Two" }]} />
Usage
Each tab has a muted label and an overlay copy in the active colour, clipped to wherever the pill
sits. As the pill slides, the colour changes exactly at its edges. An icon shows only on the active
tab: its slot opens and the icon fades in from small and blurred. Give tabs content and the panel
below fades in when the tab changes.
import { LayoutDashboard, Settings } from "lucide-react"
import { SlidingTabs } from "@/components/ui/sliding-tabs"
export function Workspace() {
return (
<SlidingTabs
label="Workspace"
tabs={[
{ value: "overview", label: "Overview", icon: <LayoutDashboard />, content: <Overview /> },
{ value: "settings", label: "Settings", icon: <Settings />, content: <SettingsForm /> },
]}
/>
)
}
Phones and tablets
When the labels don't fit, the tabs collapse to icons: every tab shows its icon, and the labels
stay readable to screen readers and as tooltips. Try the Desktop, Tablet and Mobile buttons in the
preview. It needs every tab to have an icon; set collapse="never" to keep labels, or
collapse="always" for an icon bar.
Controlled
Leave out content to use the tab list on its own, for example to switch views you render yourself.
Control it with value and onValueChange:
const [view, setView] = useState("grid")
<SlidingTabs
label="View"
value={view}
onValueChange={setView}
tabs={[{ value: "grid", label: "Grid" }, { value: "list", label: "List" }]}
/>
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
tabs | { value; label; icon?; content? }[] | — | One per tab. An icon shows only while its tab is active. |
value | string | — | Selected tab. Leave unset for uncontrolled use. |
defaultValue | string | First tab | Starting tab when uncontrolled. |
onValueChange | (value: string) => void | — | Called when a tab is chosen. |
label | string | "Tabs" | Accessible name for the tab list. |
collapse | "auto" | "never" | "always" | "auto" | Show icons instead of labels: when they don't fit, never, or always. |
className | string | — | Applied to the wrapper. |
panelClassName | string | — | Applied to the panel. |
Accessibility
- A real
tablistwithtabandtabpanelroles, linked byaria-controlsandaria-labelledby - One Tab stop: Left and Right arrows, Home and End move between tabs and select them
- The active-colour label copy is hidden from screen readers, so each tab is read once
- Collapsed to icons, each tab keeps its label for screen readers and shows it as a tooltip
- With reduced motion, the pill, labels, icons and panel change instantly