Expandable Tabs
圖示式分頁導覽,選中時圖示平滑展開為文字,支援分隔線與受控/非受控模式。
把這個效果裝進你的專案
在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。
npx webberui@latest add expandable-tabsExpandable Tabs 是用 React、Tailwind CSS 與 Motion 做的可展開圖示分頁(expandable tabs/icon tab bar)元件:平時只顯示圖示,選中的那一顆以 spring 動畫展開出文字標題,適合空間有限的工具列與導覽列。
Playground
即時調整 props、程式碼片段同步更新——直接試出你要的樣子再複製。
<ExpandableTabs/>
完整範例包含預覽的排版與資料。先安裝元件,再把範例檔貼入你的專案。
適用情境
- 行動版網頁或 PWA 的底部導覽列,按鈕多但寬度有限。
- 編輯器或白板工具的浮動工具列,用分隔線把工具分組。
- Dashboard 頂部的檢視切換,例如總覽、報表、設定。
- 聊天或協作 App 的側邊快捷操作,只在選中時顯示名稱。
安裝
npx webberui@latest add https://webberui.com/r/expandable-tabs.json或在 components.json 設定 registries 後,改用 @webberui/expandable-tabs 安裝。
使用
import { Bell, Home, Settings, User } from "lucide-react";
import {
ExpandableTabs,
type ExpandableTabItem,
} from "@/components/ui/expandable-tabs";
const tabs: ExpandableTabItem[] = [
{ title: "首頁", icon: <Home /> },
{ title: "通知", icon: <Bell /> },
{ type: "separator" },
{ title: "設定", icon: <Settings /> },
{ title: "個人", icon: <User /> },
];
export function Nav() {
return <ExpandableTabs tabs={tabs} label="主導覽" />;
}受控模式
傳入 selected 與 onChange 即進入受控模式,可與其他狀態同步:
const [selected, setSelected] = React.useState<number | null>(0);
<ExpandableTabs tabs={tabs} selected={selected} onChange={setSelected} />;範例
搭配 Next.js App Router 切換路由
以 usePathname() 算出目前的索引傳給 selected,onChange 時用 router.push 換頁。collapsible={false} 與 deselectOnOutsideClick={false} 讓目前頁面的分頁一直保持展開。
"use client";
import { usePathname, useRouter } from "next/navigation";
import { BarChart3, Home, Settings } from "lucide-react";
import { ExpandableTabs, type ExpandableTabItem } from "@/components/ui/expandable-tabs";
const routes = ["/dashboard", "/dashboard/reports", "/dashboard/settings"];
const tabs: ExpandableTabItem[] = [
{ title: "總覽", icon: <Home /> },
{ title: "報表", icon: <BarChart3 /> },
{ title: "設定", icon: <Settings /> },
];
export function DashboardNav() {
const pathname = usePathname();
const router = useRouter();
const index = routes.indexOf(pathname);
return (
<ExpandableTabs
tabs={tabs}
label="後台導覽"
selected={index === -1 ? null : index}
onChange={(i) => i !== null && router.push(routes[i])}
collapsible={false}
deselectOnOutsideClick={false}
/>
);
}有分隔線時的索引
onChange 帶入的是 tabs 陣列的索引,分隔線也佔一個位置。下例中「刪除」的索引是 3,不是 2。
const tabs: ExpandableTabItem[] = [
{ title: "複製", icon: <Copy /> },
{ title: "分享", icon: <Share2 /> },
{ type: "separator" },
{ title: "刪除", icon: <Trash2 /> },
];
<ExpandableTabs
tabs={tabs}
label="檔案操作"
onChange={(i) => {
if (i === 3) openDeleteDialog();
}}
/>自訂選中顏色
activeClassName 設定選中分頁的文字與圖示顏色,className 覆寫外框樣式。
<ExpandableTabs
tabs={tabs}
defaultSelected={0}
activeClassName="text-blue-600 dark:text-blue-400"
className="rounded-full shadow-none"
/>Props
| Prop | 型別 | 預設值 | 說明 |
|---|---|---|---|
tabs | ExpandableTabItem[] | — | 分頁清單,可混入 { type: "separator" } 分隔線 |
selected | number | null | — | 受控選中索引,null 表示全部收合 |
defaultSelected | number | null | null | 非受控模式的初始選中索引 |
onChange | (index: number | null) => void | — | 選中變更時觸發 |
collapsible | boolean | true | 再次點擊當前選中的分頁是否收合 |
deselectOnOutsideClick | boolean | true | 點擊元件外部時是否收合 |
label | string | — | 無障礙群組標籤(role="toolbar") |
activeClassName | string | — | 選中分頁的強調色 class(文字/圖示) |
className | string | — | 附加到容器的 class |
其中 ExpandableTabItem 為 { title: string; icon: React.ReactNode } 或 { type: "separator" }。
細節
- 選中的分頁以 spring 轉場同步展開容器內距與標題寬度,收合時反向 morph
- 圖示以
React.ReactNode傳入,可搭配任意圖示庫;SVG 會自動限制為size-5 - 同時只有一顆分頁可被
null選中,未選中時全部收合為純圖示
可及性
- 容器為
role="toolbar",可透過label提供aria-label - 每顆分頁皆有
aria-label與aria-pressed,展開文字時仍保有標籤 - 方向鍵(←/→/↑/↓)與 Home/End 於分頁間移動焦點(roving tabindex),自動跳過分隔線
- 使用者系統開啟「減少動態效果」時,展開/收合改為即時切換,不播放位移動畫
常見問題
可以在 Next.js App Router 使用嗎?
可以。原始碼已有 "use client",在 Server Component 頁面直接 import 即可;若要用 onChange 切換路由,包一個自己的 client component(如上方範例)。
怎麼讓選中的分頁不會被收合?
預設再點一次選中的分頁、或點擊元件外部都會收合。設定 collapsible={false} 與 deselectOnOutsideClick={false} 就會一直保持選中。
可以用鍵盤操作嗎?
可以。分頁使用 roving tabindex,Tab 只會停在其中一顆,方向鍵與 Home、End 在分頁間移動焦點並自動跳過分隔線,Enter 或 Space 選取。每顆分頁都有 aria-label 與 aria-pressed。
可以用 lucide-react 以外的圖示嗎?
可以。icon 接受任何 React.ReactNode,裡面的 SVG 會被限制為 size-5。
這是 ARIA 的 tablist 嗎?
不是。容器是 role="toolbar",每顆按鈕用 aria-pressed 表示狀態,元件不包含分頁內容面板;要顯示對應內容,依 onChange 的索引自行渲染。