WebberUI

Expandable Tabs

Icon-based tab navigation where the selected icon smoothly expands into text, with separator support and controlled and uncontrolled modes.

Loading preview…
npx shadcn@latest add https://webberui.com/r/expandable-tabs.json

Playground

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.json

Or, 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

PropTypeDefaultDescription
tabsExpandableTabItem[]The tab list; you can mix in { type: "separator" } separators
selectednumber | nullControlled selected index; null means everything is collapsed
defaultSelectednumber | nullnullInitial selected index in uncontrolled mode
onChange(index: number | null) => voidFires when the selection changes
collapsiblebooleantrueWhether clicking the currently selected tab again collapses it
deselectOnOutsideClickbooleantrueWhether clicking outside the component collapses it
labelstringAccessible group label (role="toolbar")
activeClassNamestringAccent color class for the selected tab (text / icon)
classNamestringAppended 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 to size-5
  • Only one tab can be selected at a time, and null collapses everything back to plain icons

Accessibility

  • The container is role="toolbar", and label supplies its aria-label
  • Every tab has an aria-label and aria-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

On this page