Admin 元件

Side Menu

參考
後台側邊導覽。展開/收合、分組、巢狀子項目、行動裝置抽屜。

安裝

Terminal
pnpm dlx shadcn-vue add @admin/side-menu
import {  AdminSideMenu,  AdminSideMenuContent,  AdminSideMenuFooter,  AdminSideMenuGroup,  AdminSideMenuGroupAction,  AdminSideMenuGroupContent,  AdminSideMenuGroupLabel,  AdminSideMenuHeader,  AdminSideMenuInput,  AdminSideMenuInset,  AdminSideMenuList,  AdminSideMenuListAction,  AdminSideMenuListBadge,  AdminSideMenuListButton,  AdminSideMenuListButtonChild,  AdminSideMenuListItem,  AdminSideMenuListSkeleton,  AdminSideMenuListSub,  AdminSideMenuListSubButton,  AdminSideMenuListSubItem,  AdminSideMenuProvider,  AdminSideMenuRail,  AdminSideMenuSeparator,  AdminSideMenuTrigger,  useSideMenu,} from '@/components/admin/side-menu'

用法

外層需先以 admin-side-menu-provider 提供 context,再以 admin-side-menu 結合 group / list / item / button 組合導覽結構。

<script setup lang="ts">import {  IconChevronDown,  IconChevronRight,  IconComponents,  IconLayoutDashboard,  IconSettings} from '@tabler/icons-vue'import { ref } from 'vue'import {  AdminSideMenu,  AdminSideMenuContent,  AdminSideMenuGroup,  AdminSideMenuList,  AdminSideMenuListButton,  AdminSideMenuListItem,  AdminSideMenuListSub,  AdminSideMenuListSubButton,  AdminSideMenuListSubItem,  AdminSideMenuProvider} from '@/components/admin/side-menu'​const componentsExpanded = ref(true)</script>​<template>  <admin-side-menu-provider class="block h-72 w-60">    <admin-side-menu collapsible="none">      <admin-side-menu-content>        <admin-side-menu-group>          <admin-side-menu-list>            <admin-side-menu-list-item>              <admin-side-menu-list-button>                <icon-layout-dashboard class="size-4 shrink-0" />                Dashboard              </admin-side-menu-list-button>            </admin-side-menu-list-item>​            <admin-side-menu-list-item>              <admin-side-menu-list-button                is-active                @click="componentsExpanded = !componentsExpanded"              >                <icon-components class="size-4 shrink-0" />                <span class="flex-1">Components</span>                <icon-chevron-down v-if="componentsExpanded" class="size-4 shrink-0" />                <icon-chevron-right v-else class="size-4 shrink-0" />              </admin-side-menu-list-button>​              <admin-side-menu-list-sub v-show="componentsExpanded">                <admin-side-menu-list-sub-item>                  <admin-side-menu-list-sub-button is-active                    >Button</admin-side-menu-list-sub-button                  >                </admin-side-menu-list-sub-item>                <admin-side-menu-list-sub-item>                  <admin-side-menu-list-sub-button>Input</admin-side-menu-list-sub-button>                </admin-side-menu-list-sub-item>                <admin-side-menu-list-sub-item>                  <admin-side-menu-list-sub-button>Dialog</admin-side-menu-list-sub-button>                </admin-side-menu-list-sub-item>              </admin-side-menu-list-sub>            </admin-side-menu-list-item>​            <admin-side-menu-list-item>              <admin-side-menu-list-button>                <icon-settings class="size-4 shrink-0" />                Settings              </admin-side-menu-list-button>            </admin-side-menu-list-item>          </admin-side-menu-list>        </admin-side-menu-group>      </admin-side-menu-content>    </admin-side-menu>  </admin-side-menu-provider></template>

場景

基本結構

<script setup lang="ts">import { IconComponents, IconLayoutDashboard, IconSettings } from '@tabler/icons-vue'import {  AdminSideMenu,  AdminSideMenuContent,  AdminSideMenuGroup,  AdminSideMenuList,  AdminSideMenuListButton,  AdminSideMenuListItem,  AdminSideMenuProvider} from '@/components/admin/side-menu'​const items = [  { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: false },  { key: 'components', label: 'Components', icon: IconComponents, active: true },  { key: 'settings', label: 'Settings', icon: IconSettings, active: false }]</script>​<template>  <admin-side-menu-provider class="block h-64 w-60">    <admin-side-menu collapsible="none">      <admin-side-menu-content>        <admin-side-menu-group>          <admin-side-menu-list>            <admin-side-menu-list-item v-for="item in items" :key="item.key">              <admin-side-menu-list-button :is-active="item.active">                <component :is="item.icon" class="size-4 shrink-0" />                {{ item.label }}              </admin-side-menu-list-button>            </admin-side-menu-list-item>          </admin-side-menu-list>        </admin-side-menu-group>      </admin-side-menu-content>    </admin-side-menu>  </admin-side-menu-provider></template>

完整組成

header/footer/group-label/group-action/list-badge/list-action/separator/input/skeleton。

<script setup lang="ts">import {  IconDots,  IconInbox,  IconLayoutDashboard,  IconPlus,  IconSearch,  IconSettings,  IconUserCircle} from '@tabler/icons-vue'import {  AdminSideMenu,  AdminSideMenuContent,  AdminSideMenuFooter,  AdminSideMenuGroup,  AdminSideMenuGroupAction,  AdminSideMenuGroupContent,  AdminSideMenuGroupLabel,  AdminSideMenuHeader,  AdminSideMenuInput,  AdminSideMenuList,  AdminSideMenuListAction,  AdminSideMenuListBadge,  AdminSideMenuListButton,  AdminSideMenuListItem,  AdminSideMenuListSkeleton,  AdminSideMenuProvider,  AdminSideMenuSeparator} from '@/components/admin/side-menu'​const keyword = ref('')​const items = [  { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: true, badge: '' },  { key: 'inbox', label: 'Inbox', icon: IconInbox, active: false, badge: '12' },  { key: 'settings', label: 'Settings', icon: IconSettings, active: false, badge: '' }]</script>​<template>  <admin-side-menu-provider class="block h-96 w-64">    <admin-side-menu collapsible="none">      <admin-side-menu-header>        <admin-side-menu-input v-model="keyword" placeholder="搜尋…">          <template #prefix>            <icon-search class="size-4 shrink-0 text-text-50" />          </template>        </admin-side-menu-input>      </admin-side-menu-header>​      <admin-side-menu-separator />​      <admin-side-menu-content>        <admin-side-menu-group>          <admin-side-menu-group-label>導覽</admin-side-menu-group-label>          <admin-side-menu-group-action aria-label="新增">            <icon-plus />          </admin-side-menu-group-action>          <admin-side-menu-group-content>            <admin-side-menu-list>              <admin-side-menu-list-item v-for="item in items" :key="item.key">                <admin-side-menu-list-button :is-active="item.active">                  <component :is="item.icon" class="size-4 shrink-0" />                  {{ item.label }}                </admin-side-menu-list-button>                <admin-side-menu-list-badge v-if="item.badge">{{                  item.badge                }}</admin-side-menu-list-badge>                <admin-side-menu-list-action v-else aria-label="更多">                  <icon-dots />                </admin-side-menu-list-action>              </admin-side-menu-list-item>            </admin-side-menu-list>          </admin-side-menu-group-content>        </admin-side-menu-group>​        <admin-side-menu-group>          <admin-side-menu-group-label>載入中</admin-side-menu-group-label>          <admin-side-menu-group-content>            <admin-side-menu-list>              <admin-side-menu-list-item v-for="n in 3" :key="n">                <admin-side-menu-list-skeleton show-icon />              </admin-side-menu-list-item>            </admin-side-menu-list>          </admin-side-menu-group-content>        </admin-side-menu-group>      </admin-side-menu-content>​      <admin-side-menu-footer>        <admin-side-menu-list>          <admin-side-menu-list-item>            <admin-side-menu-list-button>              <icon-user-circle class="size-4 shrink-0" />              帳號            </admin-side-menu-list-button>          </admin-side-menu-list-item>        </admin-side-menu-list>      </admin-side-menu-footer>    </admin-side-menu>  </admin-side-menu-provider></template>

變體與收合

variant:default/floating/inset;collapsible:icon/offcanvas。以 admin-side-menu-trigger 或 admin-side-menu-rail 切換展開/收合。

<script setup lang="ts">import type { AdminSideMenuProps } from '@/components/admin/side-menu'import {  IconFolder,  IconInbox,  IconLayoutDashboard,  IconLayoutSidebar,  IconSettings} from '@tabler/icons-vue'import { ref } from 'vue'import { cn } from '@/lib/utils'import {  AdminSideMenu,  AdminSideMenuContent,  AdminSideMenuFooter,  AdminSideMenuGroup,  AdminSideMenuHeader,  AdminSideMenuInset,  AdminSideMenuList,  AdminSideMenuListBadge,  AdminSideMenuListButton,  AdminSideMenuListItem,  AdminSideMenuProvider,  AdminSideMenuRail,  AdminSideMenuTrigger} from '@/components/admin/side-menu'​type Variant = NonNullable<AdminSideMenuProps['variant']>type Collapsible = 'icon' | 'offcanvas'​const VARIANT_OPTIONS: readonly Variant[] = ['default', 'floating', 'inset']const COLLAPSIBLE_OPTIONS: readonly Collapsible[] = ['icon', 'offcanvas']​const variant = ref<Variant>('default')const collapsible = ref<Collapsible>('icon')const mobileSheet = ref(true)​const items = [  { key: 'dashboard', label: 'Dashboard', icon: IconLayoutDashboard, active: true, badge: '' },  { key: 'inbox', label: 'Inbox', icon: IconInbox, active: false, badge: '8' },  { key: 'projects', label: 'Projects', icon: IconFolder, active: false, badge: '' },  { key: 'settings', label: 'Settings', icon: IconSettings, active: false, badge: '' }]​const rowClass = 'flex flex-col gap-2 sm:flex-row sm:items-center'const brandIconClass =  'flex size-8 shrink-0 items-center justify-center rounded-md bg-primary-50 text-text-10'const brandTextClass = 'truncate typo-title-small text-text-90 group-data-[collapsible=icon]:hidden'const userTextClass =  'truncate typo-content-medium text-text-70 group-data-[collapsible=icon]:hidden'const triggerClass =  'flex size-8 cursor-pointer items-center justify-center rounded-md text-text-70 hover:bg-bg-60'​const optionClass = (selected: boolean) =>  cn(    'cursor-pointer rounded-md border px-3 py-1.5 typo-content-medium transition-colors',    selected      ? 'border-primary-50 bg-primary-10 text-primary-50'      : `        border-text-30 bg-bg-90 text-text-70        hover:border-text-40      `  )</script>​<template>  <div class="flex flex-col gap-4">    <div class="flex flex-col gap-3">      <div :class="rowClass">        <span class="w-24 shrink-0 typo-content-medium text-text-70">variant</span>        <div class="flex flex-wrap gap-1.5">          <button            v-for="opt in VARIANT_OPTIONS"            :key="opt"            type="button"            :class="optionClass(variant === opt)"            @click="variant = opt"          >            {{ opt }}          </button>        </div>      </div>​      <div :class="rowClass">        <span class="w-24 shrink-0 typo-content-medium text-text-70">collapsible</span>        <div class="flex flex-wrap gap-1.5">          <button            v-for="opt in COLLAPSIBLE_OPTIONS"            :key="opt"            type="button"            :class="optionClass(collapsible === opt)"            @click="collapsible = opt"          >            {{ opt }}          </button>        </div>      </div>​      <div :class="rowClass">        <span class="w-24 shrink-0 typo-content-medium text-text-70">mobileSheet</span>        <div class="flex flex-wrap gap-1.5">          <button type="button" :class="optionClass(mobileSheet)" @click="mobileSheet = true">            true          </button>          <button type="button" :class="optionClass(!mobileSheet)" @click="mobileSheet = false">            false          </button>        </div>      </div>    </div>​    <div      :class="        cn(          'relative h-96 w-full overflow-hidden rounded-lg border border-text-20',          'transform-[translateZ(0)]'        )      "    >      <admin-side-menu-provider class="size-full min-h-0" :mobile-sheet="mobileSheet">        <admin-side-menu :collapsible="collapsible" :variant="variant">          <admin-side-menu-header>            <div class="flex h-11 items-center gap-2 px-1">              <div :class="brandIconClass">                <icon-layout-sidebar class="size-5" />              </div>              <span :class="brandTextClass">Admin</span>            </div>          </admin-side-menu-header>​          <admin-side-menu-content>            <admin-side-menu-group>              <admin-side-menu-list>                <admin-side-menu-list-item v-for="item in items" :key="item.key">                  <admin-side-menu-list-button :is-active="item.active" :tooltip="item.label">                    <component :is="item.icon" class="size-4 shrink-0" />                    <span>{{ item.label }}</span>                  </admin-side-menu-list-button>                  <admin-side-menu-list-badge v-if="item.badge">{{                    item.badge                  }}</admin-side-menu-list-badge>                </admin-side-menu-list-item>              </admin-side-menu-list>            </admin-side-menu-group>          </admin-side-menu-content>​          <admin-side-menu-footer>            <div class="flex h-11 items-center gap-2 px-1">              <div class="size-8 shrink-0 rounded-full bg-bg-60"></div>              <span :class="userTextClass">paul.chen</span>            </div>          </admin-side-menu-footer>​          <admin-side-menu-rail />        </admin-side-menu>​        <admin-side-menu-inset>          <div class="flex h-12 shrink-0 items-center gap-2 border-b border-text-20 px-3">            <admin-side-menu-trigger :class="triggerClass">              <icon-layout-sidebar class="size-5" />            </admin-side-menu-trigger>            <span class="typo-title-small text-text-90">主內容</span>          </div>          <div class="flex-1 p-4 typo-content-medium text-text-50">            切換上方 variant / collapsible,或點左上角按鈕與側邊 rail 觀察收合效果。mobileSheet            僅在瀏覽器視窗寬度 &lt; 768px 時生效:true 於手機以 Sheet 呈現,false 則維持電腦版            offcanvas 樣式。          </div>        </admin-side-menu-inset>      </admin-side-menu-provider>    </div>  </div></template>

API 參考

組成

<template>  <admin-side-menu-provider>    <admin-side-menu>      <admin-side-menu-content>        <admin-side-menu-group>          <admin-side-menu-list>            <admin-side-menu-list-item>              <admin-side-menu-list-button>導覽項目</admin-side-menu-list-button>            </admin-side-menu-list-item>          </admin-side-menu-list>        </admin-side-menu-group>      </admin-side-menu-content>    </admin-side-menu>    <admin-side-menu-inset>      <admin-side-menu-trigger />      主內容    </admin-side-menu-inset>  </admin-side-menu-provider></template>

admin-side-menu

Props

名稱型別預設值說明
side"right" | "left" | undefined"left"側選單對齊方向
variant"default" | "inset" | "floating" | undefined"default"視覺樣式變體
collapsible"none" | "icon" | "offcanvas" | undefined"icon"收合行為(offcanvas / icon / none 之一)
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{ isMobile: boolean; state: "expanded" | "collapsed"; }—

admin-side-menu-content

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{ state: "expanded" | "collapsed"; }—

admin-side-menu-group

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-inset

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-button

Props

名稱型別預設值說明
asChildboolean | undefinedfalseChange the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
isActiveboolean | undefinedfalse設為 `true` 時套用 active 狀態樣式
classClassValue—根元素之自訂 class
tooltipstring | globalThis.Component | undefined—收合狀態(collapsed icon-only)時顯示之 tooltip 文字或元件

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-button-child

Props

名稱型別預設值說明
asChildboolean | undefined—Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
isActiveboolean | undefined—設為 `true` 時套用 active 狀態樣式
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-item

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-sub

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-sub-button

Props

名稱型別預設值說明
asChildboolean | undefined—Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"a"The element or component this component should render as. Can be overwritten by `asChild`.
isActiveboolean | undefined—設為 `true` 時套用 active 狀態樣式
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-sub-item

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-provider

Props

名稱型別預設值說明
defaultOpenboolean | undefinedtrueuncontrolled 模式之初始開啟狀態
openboolean | undefined—controlled 模式之開啟狀態(以 `v-model:open` 綁定)
mobileSheetboolean | undefinedtrue手機(`< 768px`)是否以 Sheet 渲染;設為 `false` 時關閉手機 Sheet,所有 variant/collapsible 在全寬度維持電腦版樣式,預設 `true`
classClassValue—根元素之自訂 class

Events

名稱Payload說明
update:openboolean開啟狀態變更時觸發

Slots

名稱Bindings說明
default{}—

admin-side-menu-trigger

Props

名稱型別預設值說明
asChildboolean | undefined—Change the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-rail

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-header

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-group-label

Props

名稱型別預設值說明
asChildboolean | undefinedfalseChange the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"div"The element or component this component should render as. Can be overwritten by `asChild`.
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-group-content

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-group-action

Props

名稱型別預設值說明
asChildboolean | undefinedfalseChange the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-action

Props

名稱型別預設值說明
asChildboolean | undefinedfalseChange the default rendered element for the one passed as a child, merging their props and behavior. Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details.
asglobalThis.Component | AsTag | undefined"button"The element or component this component should render as. Can be overwritten by `asChild`.
showOnHoverboolean | undefinedfalse設為 `true` 時僅在項目 hover / focus / 開啟狀態顯示
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-list-badge

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

Slots

名稱Bindings說明
default{}—

admin-side-menu-input

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

admin-side-menu-separator

Props

名稱型別預設值說明
classClassValue—根元素之自訂 class

admin-side-menu-list-skeleton

Props

名稱型別預設值說明
showIconboolean | undefinedfalse是否顯示前置 icon 佔位
widthstring | undefined"70%"文字佔位寬度(固定值,避免 SSR hydration mismatch)
classClassValue—根元素之自訂 class

更新紀錄

  • fix: 與上游同步修正 shadcn-vue 的收合點擊與浮層可用寬af0690c
  • feat: 補齊 shadcn sidebar 11 個組成並新增 mobileSheet 開關41b96eb