導航

Expandable Tabs

圖示式分頁導覽,選中時圖示平滑展開為文字,支援分隔線與受控/非受控模式。

把這個效果裝進你的專案

在專案資料夾執行指令,CLI 會加入原始碼與依賴套件,再依下方使用範例引用元件。

npx webberui@latest add expandable-tabs

還沒有專案,或安裝遇到問題?看快速開始 →

Expandable 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型別預設值說明
tabsExpandableTabItem[]—分頁清單,可混入 { type: "separator" } 分隔線
selectednumber | null—受控選中索引,null 表示全部收合
defaultSelectednumber | nullnull非受控模式的初始選中索引
onChange(index: number | null) => void—選中變更時觸發
collapsiblebooleantrue再次點擊當前選中的分頁是否收合
deselectOnOutsideClickbooleantrue點擊元件外部時是否收合
labelstring—無障礙群組標籤(role="toolbar")
activeClassNamestring—選中分頁的強調色 class(文字/圖示)
classNamestring—附加到容器的 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 的索引自行渲染。