Expandable Tabs
Icon-based tab navigation where the selected icon smoothly expands into text, with separator support and controlled and uncontrolled modes.
npx shadcn@latest add https://webberui.com/r/expandable-tabs.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ExpandableTabs />
Installation
npx shadcn@latest add https://webberui.com/r/expandable-tabs.jsonOr, once registries are configured in components.json, install it as @webberui/expandable-tabs.
Usage
import { Bell, Home, Settings, User } from "lucide-react";
import {
ExpandableTabs,
type ExpandableTabItem,
} from "@/components/ui/expandable-tabs";
const tabs: ExpandableTabItem[] = [
{ title: "Home", icon: <Home /> },
{ title: "Alerts", icon: <Bell /> },
{ type: "separator" },
{ title: "Settings", icon: <Settings /> },
{ title: "Profile", icon: <User /> },
];
export function Nav() {
return <ExpandableTabs tabs={tabs} label="Main navigation" />;
}controlled mode
Passing selected and onChange puts it in controlled mode, so it can stay in sync with other state:
const [selected, setSelected] = React.useState<number | null>(0);
<ExpandableTabs tabs={tabs} selected={selected} onChange={setSelected} />;Props
| Prop | Type | Default | Description |
|---|---|---|---|
tabs | ExpandableTabItem[] | — | The tab list; you can mix in { type: "separator" } separators |
selected | number | null | — | Controlled selected index; null means everything is collapsed |
defaultSelected | number | null | null | Initial selected index in uncontrolled mode |
onChange | (index: number | null) => void | — | Fires when the selection changes |
collapsible | boolean | true | Whether clicking the currently selected tab again collapses it |
deselectOnOutsideClick | boolean | true | Whether clicking outside the component collapses it |
label | string | — | Accessible group label (role="toolbar") |
activeClassName | string | — | Accent color class for the selected tab (text / icon) |
className | string | — | Appended to the container's className |
ExpandableTabItem is either { title: string; icon: React.ReactNode } or { type: "separator" }.
How it works
- The selected tab expands the container padding and the title width together with a spring transition, morphing back in reverse when it collapses
- Icons are passed in as
React.ReactNode, so any icon library works; SVGs are automatically constrained tosize-5 - Only one tab can be selected at a time, and
nullcollapses everything back to plain icons
Accessibility
- The container is
role="toolbar", andlabelsupplies itsaria-label - Every tab has an
aria-labelandaria-pressed, and keeps its label even when the text is expanded - The arrow keys (←/→/↑/↓) plus Home/End move focus between tabs (roving tabindex), skipping separators automatically
- When the user has "reduce motion" enabled at the system level, expanding and collapsing become instant switches with no displacement animation
Dynamic Island Nav
A Dynamic Island-style pill navigation — the selected pill slides and morphs between items, the selected item's name expands from 0, and one more click morphs the island downward into a panel.
Gooey Menu
A liquid, gooey expanding menu — click the floating trigger and the items are "pulled" out of the button through an SVG goo filter and fuse together, with four directions plus radial fanning.