Sidebar 侧边栏

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

创作者中心

概览

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

'use client'

import { useState } from 'react'
import { FileText, House, Images, Settings } from 'lucide-react'
import {
  AppShell,
  Avatar,
  Heading,
  Inline,
  NavLink,
  Sidebar,
  SidebarGroup,
  SidebarLabel,
  SidebarTrigger,
  Stack,
  Text,
} from '@hina-ui/react'

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 }] },
]

export default function Demo() {
  const [selected, setSelected] = useState('overview')
  return (
    <AppShell
      mobileTitle="Hina UI"
      className="border-line h-112 w-full rounded-lg border"
      sidebarContent={
        <Sidebar
          renderIcon={() => <Avatar src="/favicon.png" name="Hina UI" className="rounded-md" />}
          renderWordmark={() => (
            <Stack gap="none">
              <Text weight="medium" className="truncate">
                Hina UI
              </Text>
              <Text size="xs" tone="muted" className="truncate">
                工作空间
              </Text>
            </Stack>
          )}
          renderFooter={() => (
            <Inline gap="sm" align="center" wrap={false}>
              <Avatar src="/avatars/paper.webp" name="星见书音" />
              <SidebarLabel as="div" className="flex-1">
                <Stack gap="none">
                  <Text size="sm" weight="medium" className="truncate">
                    星见书音
                  </Text>
                  <Text size="xs" tone="muted" className="truncate">
                    管理员
                  </Text>
                </Stack>
              </SidebarLabel>
            </Inline>
          )}
        >
          {groups.map(group => (
            <SidebarGroup key={group.label} label={group.label}>
              {group.items.map(item => (
                <NavLink
                  key={item.id}
                  href={`#${item.id}`}
                  label={item.label}
                  active={selected === item.id}
                  icon={<item.icon />}
                  onClick={event => {
                    event.preventDefault()
                    setSelected(item.id)
                  }}
                >
                  {item.label}
                </NavLink>
              ))}
            </SidebarGroup>
          ))}
        </Sidebar>
      }
      header={
        <>
          <SidebarTrigger />
          <Heading level={2} size="sm" className="truncate">
            创作者中心
          </Heading>
        </>
      }
    >
      <Stack gap="sm" className="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>
  )
}
tsx

用法

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

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

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

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

import { AppShell, NavLink, Sidebar, Text } from '@hina-ui/react'

export default function Demo() {
  return (
    <AppShell
      className="border-line h-64 w-full rounded-lg border"
      sidebarContent={
        <Sidebar>
          <NavLink href="#" active label="概览">
            概览
          </NavLink>
          <NavLink href="#" label="我的书架">
            我的书架
          </NavLink>
          <NavLink href="#" label="收藏">
            收藏
          </NavLink>
        </Sidebar>
      }
    >
      <Text size="sm" tone="muted" className="block p-6">
        侧栏条目放在默认插槽里。
      </Text>
    </AppShell>
  )
}
tsx

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

示例

分组

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

「创作」一组默认收起。

import { AppShell, NavLink, Sidebar, SidebarGroup, Text } from '@hina-ui/react'

export default function Demo() {
  return (
    <AppShell
      className="border-line h-96 w-full rounded-lg border"
      sidebarContent={
        <Sidebar>
          <NavLink href="#" active label="概览">
            概览
          </NavLink>
          <SidebarGroup label="作品库">
            <NavLink href="#" label="Galgame">
              Galgame
            </NavLink>
            <NavLink href="#" label="轻小说">
              轻小说
            </NavLink>
          </SidebarGroup>
          <SidebarGroup label="创作" defaultOpen={false}>
            <NavLink href="#" label="我的条目">
              我的条目
            </NavLink>
            <NavLink href="#" label="草稿箱">
              草稿箱
            </NavLink>
          </SidebarGroup>
        </Sidebar>
      }
    >
      <Text size="sm" tone="muted" className="block p-6">
        「创作」一组默认收起。
      </Text>
    </AppShell>
  )
}
tsx

品牌图标与字标

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

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

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

两个函数均接收 { state }。传入 renderHeader 时由它完全接管页眉,renderIcon 和 renderWordmark 不再渲染。

图标与字标

品牌插槽

图标与字标

只有字标

品牌插槽

只有字标

只有图标

品牌插槽

只有图标

'use client'

import { House } from 'lucide-react'
import { AppShell, Image, NavLink, Sidebar, SidebarTrigger, Stack, Text } from '@hina-ui/react'
import { Wordmark } from '../../../components/Wordmark'

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

export default function Demo() {
  return (
    <Stack gap="lg" className="w-full">
      {examples.map(example => (
        <Stack key={example.mode} gap="sm">
          <Text size="sm" tone="muted">
            {example.label}
          </Text>
          <AppShell
            mobileTitle="Hina UI"
            className="border-line h-52 w-full rounded-lg border"
            sidebarContent={
              <Sidebar
                renderIcon={
                  example.mode !== 'wordmark'
                    ? () => <Image src="/favicon.png" alt="Hina UI" className="rounded-md" />
                    : undefined
                }
                renderWordmark={
                  example.mode !== 'icon' ? () => <Wordmark className="items-center" /> : undefined
                }
              >
                <NavLink
                  href="#"
                  active
                  label="概览"
                  icon={<House />}
                  onClick={event => event.preventDefault()}
                >
                  概览
                </NavLink>
              </Sidebar>
            }
            header={
              <>
                <SidebarTrigger />
                <Text size="sm" weight="medium">
                  品牌插槽
                </Text>
              </>
            }
          >
            <Text size="sm" tone="muted" className="block p-6">
              {example.label}
            </Text>
          </AppShell>
        </Stack>
      ))}
    </Stack>
  )
}
tsx

页眉与页脚

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

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

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

创作者中心

概览

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

'use client'

import { useState } from 'react'
import { FileText, House, Images, Settings } from 'lucide-react'
import {
  AppShell,
  Avatar,
  Heading,
  Inline,
  NavLink,
  Sidebar,
  SidebarGroup,
  SidebarLabel,
  SidebarTrigger,
  Stack,
  Text,
} from '@hina-ui/react'

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 }] },
]

export default function Demo() {
  const [selected, setSelected] = useState('overview')
  return (
    <AppShell
      mobileTitle="Hina UI"
      className="border-line h-112 w-full rounded-lg border"
      sidebarContent={
        <Sidebar
          renderHeader={() => (
            <Inline gap="sm" align="center" wrap={false}>
              <Avatar src="/favicon.png" name="Hina UI" className="rounded-md" />
              <SidebarLabel as="div" className="flex-1">
                <Stack gap="none">
                  <Text weight="medium" className="truncate">
                    Hina UI
                  </Text>
                  <Text size="xs" tone="muted" className="truncate">
                    工作空间
                  </Text>
                </Stack>
              </SidebarLabel>
            </Inline>
          )}
          renderFooter={() => (
            <Inline gap="sm" align="center" wrap={false}>
              <Avatar src="/avatars/paper.webp" name="星见书音" />
              <SidebarLabel as="div" className="flex-1">
                <Stack gap="none">
                  <Text size="sm" weight="medium" className="truncate">
                    星见书音
                  </Text>
                  <Text size="xs" tone="muted" className="truncate">
                    管理员
                  </Text>
                </Stack>
              </SidebarLabel>
            </Inline>
          )}
        >
          {groups.map(group => (
            <SidebarGroup key={group.label} label={group.label}>
              {group.items.map(item => (
                <NavLink
                  key={item.id}
                  href={`#${item.id}`}
                  label={item.label}
                  active={selected === item.id}
                  icon={<item.icon />}
                  onClick={event => {
                    event.preventDefault()
                    setSelected(item.id)
                  }}
                >
                  {item.label}
                </NavLink>
              ))}
            </SidebarGroup>
          ))}
        </Sidebar>
      }
      header={
        <>
          <SidebarTrigger />
          <Heading level={2} size="sm" className="truncate">
            创作者中心
          </Heading>
        </>
      }
    >
      <Stack gap="sm" className="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>
  )
}
tsx

行为

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

无障碍

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

API

Sidebar

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

SidebarGroup

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

SidebarLabel

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

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

SidebarTrigger

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

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