Sidebar 侧边栏

应用左侧的导航栏,可收起为图标条。

创作者中心

概览

切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileText, House, Images, Settings } from '@lucide/vue'
  import {
    AppShell,
    Avatar,
    Heading,
    Inline,
    NavLink,
    Sidebar,
    SidebarGroup,
    SidebarLabel,
    SidebarTrigger,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const selected = ref('overview')
  const groups = [
    {
      label: '内容',
      items: [
        { id: 'overview', label: '概览', icon: House },
        { id: 'articles', label: '文章', icon: FileText },
        { id: 'library', label: '媒体库', icon: Images },
      ],
    },
    { label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
  ]
</script>

<template>
  <AppShell mobile-title="Hina UI" class="border-line h-112 w-full rounded-lg border">
    <template #sidebar>
      <Sidebar>
        <template #icon>
          <Avatar src="/favicon.png" name="Hina UI" class="rounded-md" />
        </template>
        <template #wordmark>
          <Stack gap="none">
            <Text weight="medium" class="truncate">Hina UI</Text>
            <Text size="xs" tone="muted" class="truncate">工作空间</Text>
          </Stack>
        </template>

        <SidebarGroup v-for="group in groups" :key="group.label" :label="group.label">
          <NavLink
            v-for="item in group.items"
            :key="item.id"
            :href="`#${item.id}`"
            :label="item.label"
            :active="selected === item.id"
            @click.prevent="selected = item.id"
          >
            <template #icon><component :is="item.icon" /></template>
            {{ item.label }}
          </NavLink>
        </SidebarGroup>

        <template #footer>
          <Inline gap="sm" align="center" :wrap="false">
            <Avatar src="/avatars/paper.webp" name="星见书音" />
            <SidebarLabel as="div" class="flex-1">
              <Stack gap="none">
                <Text size="sm" weight="medium" class="truncate">星见书音</Text>
                <Text size="xs" tone="muted" class="truncate">管理员</Text>
              </Stack>
            </SidebarLabel>
          </Inline>
        </template>
      </Sidebar>
    </template>
    <template #header>
      <SidebarTrigger />
      <Heading :level="2" size="sm" class="truncate">创作者中心</Heading>
    </template>
    <Stack gap="sm" class="p-6">
      <Heading :level="3" size="sm">
        {{ groups.flatMap(group => group.items).find(item => item.id === selected)?.label }}
      </Heading>
      <Text size="sm" tone="muted">切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。</Text>
    </Stack>
  </AppShell>
</template>
vue

用法

import { Sidebar, SidebarGroup, SidebarLabel, SidebarTrigger } from '@hina-ui/vue'
ts

Sidebar 放进 AppShell 的 sidebar 插槽,条目写在默认插槽里,通常是一串 NavLink。它自带导航地标与滚动容器,条目再多也只在栏内滚动。

它的形态由 AppShell 提供,自身不持有状态。脱离 AppShell 时它始终是展开形态,也无法收起。

侧栏条目放在默认插槽里。

<script setup lang="ts">
  import { AppShell, NavLink, Sidebar, Text } from '@hina-ui/vue'
</script>

<template>
  <AppShell class="border-line h-64 w-full rounded-lg border">
    <template #sidebar>
      <Sidebar>
        <NavLink href="#" active label="概览">概览</NavLink>
        <NavLink href="#" label="我的书架">我的书架</NavLink>
        <NavLink href="#" label="收藏">收藏</NavLink>
      </Sidebar>
    </template>

    <Text size="sm" tone="muted" class="block p-6">侧栏条目放在默认插槽里。</Text>
  </AppShell>
</template>
vue

侧栏内的每个 NavLink 都应当写 label:收起为 rail 后文字淡出,label 会接手成为悬停提示与无障碍名。

示例

分组

SidebarGroup 把条目归到一个可折叠的标题下。label 是组名,defaultOpen 决定初始是否展开,默认展开。

「创作」一组默认收起。

<script setup lang="ts">
  import { AppShell, NavLink, Sidebar, SidebarGroup, Text } from '@hina-ui/vue'
</script>

<template>
  <AppShell class="border-line h-96 w-full rounded-lg border">
    <template #sidebar>
      <Sidebar>
        <NavLink href="#" active label="概览">概览</NavLink>
        <SidebarGroup label="作品库">
          <NavLink href="#" label="Galgame">Galgame</NavLink>
          <NavLink href="#" label="轻小说">轻小说</NavLink>
        </SidebarGroup>
        <SidebarGroup label="创作" :default-open="false">
          <NavLink href="#" label="我的条目">我的条目</NavLink>
          <NavLink href="#" label="草稿箱">草稿箱</NavLink>
        </SidebarGroup>
      </Sidebar>
    </template>

    <Text size="sm" tone="muted" class="block p-6">「创作」一组默认收起。</Text>
  </AppShell>
</template>
vue

品牌图标与字标

icon 与 wordmark 插槽组成默认页眉。图标位固定为 32 × 32 像素,SVG 和图片按比例显示;字标可以是文字、SVG、Image 或组合内容。

插槽内容展开收起为 rail
icon + wordmark图标与字标并排显示图标保持原位,字标淡出
只有 icon显示图标保持原位
只有 wordmark字标从页眉起始位置显示品牌区整体收起
均未提供不生成品牌区不生成品牌区

有图标时,收起后品牌行保留高度;只有字标时,品牌区连同上下内边距一起收起,导航向上填补空位。重新展开或进入移动端抽屉时,品牌区完整显示。字标自动使用与导航文字相同的过渡,不需要再包 SidebarLabel。

两个插槽均提供 { state }。传入 header 时由它完全接管页眉,icon 和 wordmark 不再渲染。

图标与字标

品牌插槽

图标与字标

只有字标

品牌插槽

只有字标

只有图标

品牌插槽

只有图标

<script setup lang="ts">
  import { House } from '@lucide/vue'
  import { AppShell, Image, NavLink, Sidebar, SidebarTrigger, Stack, Text } from '@hina-ui/vue'
  import Wordmark from '../../../components/docs/Wordmark.vue'

  const examples = [
    { mode: 'both', label: '图标与字标' },
    { mode: 'wordmark', label: '只有字标' },
    { mode: 'icon', label: '只有图标' },
  ]
</script>

<template>
  <Stack gap="lg" class="w-full">
    <Stack v-for="example in examples" :key="example.mode" gap="sm">
      <Text size="sm" tone="muted">{{ example.label }}</Text>
      <AppShell mobile-title="Hina UI" class="border-line h-52 w-full rounded-lg border">
        <template #sidebar>
          <Sidebar>
            <template v-if="example.mode !== 'wordmark'" #icon>
              <Image src="/favicon.png" alt="Hina UI" class="rounded-md" />
            </template>
            <template v-if="example.mode !== 'icon'" #wordmark>
              <Wordmark class="items-center" />
            </template>
            <NavLink href="#" active label="概览" @click.prevent>
              <template #icon><House /></template>
              概览
            </NavLink>
          </Sidebar>
        </template>
        <template #header>
          <SidebarTrigger />
          <Text size="sm" weight="medium">品牌插槽</Text>
        </template>
        <Text size="sm" tone="muted" class="block p-6">{{ example.label }}</Text>
      </AppShell>
    </Stack>
  </Stack>
</template>
vue

页眉与页脚

header 完整替换默认品牌页眉,footer 位于条目区下方,两者都不随条目滚动。自定义页眉不会被自动视为 logo 隐藏。

页眉保持展开时的内容宽度。页脚随侧栏实际宽度收起,内部按钮与浮层锚点不会超出 rail。水平排布的头像和文字可使用 Inline 并设置 :wrap="false";文字使用 truncate 或 whitespace-nowrap 避免收起时换行。把文字与附属操作放进 SidebarLabel,它会与 NavLink 的文字一起淡出,展开时延后淡入;标识和 Avatar 留在外面,位置与尺寸保持不变。

SidebarLabel 不改变内容的占位。rail 形态下其内容不可见、不可交互,也不进入朗读和键盘焦点序列。header 和 footer 仍提供 { state },供自定义内容读取当前形态。

创作者中心

概览

切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。

<script setup lang="ts">
  import { ref } from 'vue'
  import { FileText, House, Images, Settings } from '@lucide/vue'
  import {
    AppShell,
    Avatar,
    Heading,
    Inline,
    NavLink,
    Sidebar,
    SidebarGroup,
    SidebarLabel,
    SidebarTrigger,
    Stack,
    Text,
  } from '@hina-ui/vue'

  const selected = ref('overview')
  const groups = [
    {
      label: '内容',
      items: [
        { id: 'overview', label: '概览', icon: House },
        { id: 'articles', label: '文章', icon: FileText },
        { id: 'library', label: '媒体库', icon: Images },
      ],
    },
    { label: '管理', items: [{ id: 'settings', label: '设置', icon: Settings }] },
  ]
</script>

<template>
  <AppShell mobile-title="Hina UI" class="border-line h-112 w-full rounded-lg border">
    <template #sidebar>
      <Sidebar>
        <template #header>
          <Inline gap="sm" align="center" :wrap="false">
            <Avatar src="/favicon.png" name="Hina UI" class="rounded-md" />
            <SidebarLabel as="div" class="flex-1">
              <Stack gap="none">
                <Text weight="medium" class="truncate">Hina UI</Text>
                <Text size="xs" tone="muted" class="truncate">工作空间</Text>
              </Stack>
            </SidebarLabel>
          </Inline>
        </template>

        <SidebarGroup v-for="group in groups" :key="group.label" :label="group.label">
          <NavLink
            v-for="item in group.items"
            :key="item.id"
            :href="`#${item.id}`"
            :label="item.label"
            :active="selected === item.id"
            @click.prevent="selected = item.id"
          >
            <template #icon><component :is="item.icon" /></template>
            {{ item.label }}
          </NavLink>
        </SidebarGroup>

        <template #footer>
          <Inline gap="sm" align="center" :wrap="false">
            <Avatar src="/avatars/paper.webp" name="星见书音" />
            <SidebarLabel as="div" class="flex-1">
              <Stack gap="none">
                <Text size="sm" weight="medium" class="truncate">星见书音</Text>
                <Text size="xs" tone="muted" class="truncate">管理员</Text>
              </Stack>
            </SidebarLabel>
          </Inline>
        </template>
      </Sidebar>
    </template>
    <template #header>
      <SidebarTrigger />
      <Heading :level="2" size="sm" class="truncate">创作者中心</Heading>
    </template>
    <Stack gap="sm" class="p-6">
      <Heading :level="3" size="sm">
        {{ groups.flatMap(group => group.items).find(item => item.id === selected)?.label }}
      </Heading>
      <Text size="sm" tone="muted">切换侧栏,观察标识、分组与页脚的位置。窄屏时打开抽屉。</Text>
    </Stack>
  </AppShell>
</template>
vue

行为

  • 三种形态的宽度分别是展开 256 像素、rail 56 像素、隐藏 0 像素,切换时宽度连续过渡。
  • 收起为 rail 时,SidebarGroup 强制展开、组标题原位淡出并显示分隔线,标题占位保持不变,已展开的条目不会随收起动作上下移动。
  • 条目区使用不带边缘阴影的 ScrollArea,页眉与页脚固定在两端。
  • 完全隐藏时,侧栏整体退出交互与键盘焦点序列。
  • 宽度和文字过渡使用 Hina 动画 token;系统开启减弱动态效果时直接切换。
  • 搬入移动端 Drawer 时,侧栏撑满抽屉高度,页脚固定在底部,仅条目区滚动。页眉内默认提供关闭按钮,设置 :closable="false" 可隐藏;未提供页眉或品牌插槽时,也不保留按钮行的占位。不重复显示抽屉标题;水平内边距由抽屉提供,页眉、条目区和页脚保留各自的纵向内边距。

无障碍

  • 条目区是 nav 地标,默认无障碍名取自界面语言(简体中文为「侧边导航」),label 可覆盖。
  • rail 形态下条目的文字虽然不可见,但 NavLink 的 label 会作为 aria-label 保留。
  • closable 仅控制移动端关闭按钮,隐藏后仍可按 Escape、点击遮罩或通过受控状态关闭抽屉。
  • 隐藏的字标退出朗读与焦点序列。品牌图标使用图片时提供 alt,使用 SVG 时提供合适的无障碍名;若图标包含链接,链接本身也应有名称。
  • 收起后被隐藏的组标题同时退出键盘序列,不会出现能聚焦却看不见的控件。

API

Sidebar

属性
类型
默认值
说明
label
string
取自界面语言
导航地标的无障碍名
closable
boolean
true
是否显示移动端抽屉的关闭按钮
class
string
—
追加到根元素的类
插槽插槽参数
说明
default—
侧栏条目
header{ state }
完整替换页眉,优先于品牌插槽
icon{ state }
固定方形区域内的品牌图标,rail 时保留
wordmark{ state }
品牌字标,rail 时自动淡出
footer{ state }
条目区下方的内容

SidebarGroup

属性
类型
默认值
说明
label
string
必填
组名
defaultOpen
boolean
true
初始是否展开
class
string
—
追加到根元素的类
插槽
说明
default
组内的条目

SidebarLabel

控制自定义文字与附属内容在 rail 形态下的显隐;不在侧栏状态提供方内时始终显示。

属性
类型
默认值
说明
as
string
'span'
渲染的元素
class
string
—
追加到根元素的类
插槽
说明
default
随侧栏收起而隐藏的内容

SidebarTrigger

切换侧栏形态的按钮,通常放在 AppShell 的 header 插槽里。它没有可配置的行为,脱离 AppShell 时不渲染。

属性
类型
默认值
说明
class
string
—
追加到根元素的类