'use client'
import { SlidersHorizontal } from 'lucide-react'
import { Button, Drawer, Input, Stack, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="筛选"
description="设定之后只显示符合条件的作品。"
renderContent={() => (
<Stack gap="sm">
<Stack gap="xs">
<Text size="sm">关键词</Text>
<Input defaultValue="天文台" />
</Stack>
<Stack gap="xs">
<Text size="sm">发行年份</Text>
<Input defaultValue="2024" />
</Stack>
<Stack gap="xs">
<Text size="sm">制作方</Text>
<Input defaultValue="ANIPLEX.EXE" />
</Stack>
</Stack>
)}
renderFooter={({ close }) => (
<>
<Button variant="soft" tone="neutral" onClick={close}>
重置
</Button>
<Button onClick={close}>应用</Button>
</>
)}
>
<Button variant="outline" tone="neutral" icon={<SlidersHorizontal />}>
筛选
</Button>
</Drawer>
)
}
用法
import { Drawer } from '@hina-ui/react'
title 必填,description 是标题下面的一行说明。children 是触发器,renderContent 渲染正文,renderFooter 渲染底部的操作按钮。renderContent、renderFooter 和接管内部布局的 renderBody 都会收到 close 方法。
'use client'
import { Button, Drawer, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="通知"
description="最近七天的消息。"
renderContent={() => <Text>这里放置消息列表,抽屉贴着屏幕边缘展开,四角保持直角。</Text>}
>
<Button variant="outline" tone="neutral">
打开通知
</Button>
</Drawer>
)
}
抽屉贴着屏幕边缘打开,占满整个高度,四角为直角。遮罩、停止页面滚动和焦点陷阱与 Dialog 相同。
示例
标题内容
与 Dialog 一样,icon 属性在标题前显示装饰图标,titleContent 属性替换标题内容,默认显示 title 属性。自定义标题保留 <h2> 语义与面板名称关联。
示例在标题中加入 Tag。
'use client'
import { Settings } from 'lucide-react'
import { Button, Inline, Drawer, Tag, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="面板标题"
icon={<Settings />}
titleContent={
<Inline as="span" wrap={false} className="inline-flex gap-2">
自定义标题
<Tag size="sm" tone="neutral">
可选
</Tag>
</Inline>
}
renderContent={() => <Text>图标独立显示,标题插槽可以组合文字和标签。</Text>}
>
<Button variant="outline" tone="neutral">
标题插槽
</Button>
</Drawer>
)
}
隐藏标题栏
设置 header={false} 隐藏整个标题栏,包括标题、说明和关闭按钮;正文与页脚保持原有布局。title 仍必填,与提供的 description 一起保留为辅助技术可读的隐藏内容,此时不渲染 icon 和 titleContent 属性。
'use client'
import { Button, Drawer, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="无头部"
header={false}
renderContent={() => <Text>标题保留为无障碍名称,正文与页脚正常显示。</Text>}
renderFooter={({ close }) => <Button onClick={close}>关闭</Button>}
>
<Button variant="outline" tone="neutral">
无头部
</Button>
</Drawer>
)
}
关闭按钮
closable 默认为 true。设为 false 只隐藏标题栏中的关闭按钮,按 Esc 和点击遮罩仍可关闭,locked 控制这些关闭行为。隐藏标题栏或提供 renderBody 时,内置关闭按钮不参与渲染。
'use client'
import { Button, Drawer, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="无关闭按钮"
closable={false}
renderContent={() => <Text>按 Esc、点击遮罩或底部按钮均可关闭。</Text>}
renderFooter={({ close }) => <Button onClick={close}>关闭</Button>}
>
<Button variant="outline" tone="neutral">
无关闭按钮
</Button>
</Drawer>
)
}
自定义面板内容
renderBody 接管面板内部布局,替换默认标题栏、正文和页脚。组件不再添加内容内边距、区域间距或 ScrollArea 包装,滚动与底部安全区留白由传入的内容控制。返回空内容也不会恢复默认布局。
此时 header、closable 以及 icon、titleContent、renderContent、renderFooter 属性不参与渲染。title 仍必填,标题与提供的说明以视觉隐藏的形式保留;遮罩、焦点约束和 locked 继续生效,渲染函数的 close() 可程序化关闭面板。
示例使用 CloseButton、ScrollArea 和 Button 组织贴边标题栏、独立滚动区域与固定底栏。
'use client'
import { Button, CloseButton, Inline, Drawer, ScrollArea, Stack, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Drawer
title="自定义布局"
description="标题与说明仍保留为无障碍内容。"
renderBody={({ close }) => (
<Stack gap="none" className="min-h-0 flex-1">
<Inline
justify="between"
wrap={false}
className="border-line bg-inset shrink-0 gap-3 border-b p-4"
>
<Stack gap="xs" className="min-w-0">
<Text weight="medium">自定义布局</Text>
<Text size="sm" tone="muted">
标题栏、滚动区域和底栏均由插槽提供。
</Text>
</Stack>
<CloseButton onClick={close} />
</Inline>
<ScrollArea className="min-h-0 flex-1">
<Stack gap="none" className="divide-line divide-y px-4">
{Array.from({ length: 30 }, (_, i) => i + 1).map(index => (
<Text key={index} className="py-3">
内容 {index}
</Text>
))}
</Stack>
</ScrollArea>
<Inline
justify="between"
wrap={false}
className="border-line shrink-0 gap-3 border-t p-4 pb-[max(var(--hn-panel-p),env(safe-area-inset-bottom))]"
>
<Text size="sm" tone="muted">
底栏保持可见
</Text>
<Button size="sm" onClick={close}>
完成
</Button>
</Inline>
</Stack>
)}
>
<Button variant="outline" tone="neutral">
自定义面板
</Button>
</Drawer>
)
}
方向
side 取 start 或 end,指文字方向上的起始边和结束边。中文和英文环境下分别是左边缘和右边缘,从右向左书写的语言中自动对调。
'use client'
import { Button, Drawer, Inline, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Inline align="center" className="gap-6">
<Drawer
title="从起始边展开"
side="start"
renderContent={() => (
<Text>中文与英文环境下贴左缘,阿拉伯语等从右到左的语言下贴右缘。</Text>
)}
>
<Button variant="outline" tone="neutral">
start
</Button>
</Drawer>
<Drawer
title="从结束边展开"
side="end"
renderContent={() => <Text>默认值,中文与英文环境下贴右缘。</Text>}
>
<Button variant="outline" tone="neutral">
end
</Button>
</Drawer>
</Inline>
)
}
尺寸
size 设置抽屉的宽度,三档分别为 288、360 和 480 像素。窄屏上宽度不会超过视口宽度减去 48 像素。
'use client'
import { Button, Drawer, Inline, Text } from '@hina-ui/react'
export default function Demo() {
return (
<Inline align="center" className="gap-6">
{(['sm', 'md', 'lg'] as const).map(size => (
<Drawer
key={size}
size={size}
title={`${size} 档`}
renderContent={() => <Text>抽屉的宽度随 size 变化,高度始终占满屏幕。</Text>}
>
<Button variant="outline" tone="neutral">
{size}
</Button>
</Drawer>
))}
</Inline>
)
}
长内容
超出可用高度的内容在 renderContent 渲染的正文区域内滚动,标题和页脚保持不动。
'use client'
import { Button, Drawer, Stack, Text } from '@hina-ui/react'
const items = Array.from(
{ length: 30 },
(_, i) => `第 ${i + 1} 条通知:这是一段用于占位的文字,好让正文足够长,能够看到滚动。`,
)
export default function Demo() {
return (
<Drawer
title="全部通知"
description="标题与页脚固定,中间的列表可以滚动。"
renderContent={() => (
<Stack gap="sm">
{items.map(item => (
<Text key={item}>{item}</Text>
))}
</Stack>
)}
renderFooter={({ close }) => (
<Button variant="soft" tone="neutral" onClick={close}>
全部标为已读
</Button>
)}
>
<Button variant="outline" tone="neutral">
查看全部
</Button>
</Drawer>
)
}
滚动容器
通过组件 ref 的 viewport 获取正文内置 ScrollArea 的实际滚动元素。可以读取 scrollTop、调用 scrollTo(),或将它交给滚动监听、观察器。
viewport 的类型为 HTMLElement | undefined。正文滚动区域初始化完成前、没有 renderContent 或内容卸载后为 undefined;退场期间仍返回当前元素,再次打开时更新为新的元素。viewport 可用或变化时 ref 句柄会重新传入;需要在可用时执行操作或绑定监听,可用 useState 的 setter 作为回调 ref,在依赖 drawer?.viewport 的 effect 中绑定,并在 effect 的清理函数中解除绑定。
使用 renderBody 属性时,内置滚动区域被替换,viewport 为 undefined;自定义滚动区域由调用方自行引用。
示例中的 Button 通过 viewport.scrollTo() 控制滚动。
'use client'
import { useCallback, useState } from 'react'
import { Button, Drawer, Stack, Text, type DrawerHandle } from '@hina-ui/react'
export default function Demo() {
const [viewport, setViewport] = useState<HTMLElement>()
const modal = useCallback((handle: DrawerHandle | null) => setViewport(handle?.viewport), [])
function scrollTo(position: 'start' | 'end') {
if (!viewport) return
viewport.scrollTo({ top: position === 'start' ? 0 : viewport.scrollHeight })
}
return (
<Drawer
ref={modal}
title="滚动容器"
description="标题与页脚固定,正文独立滚动。"
renderContent={() => (
<Stack gap="sm">
{Array.from({ length: 30 }, (_, i) => i + 1).map(index => (
<Text key={index}>第 {index} 行:正文在中间滚动,标题与页脚保持不动。</Text>
))}
</Stack>
)}
renderFooter={({ close }) => (
<>
<Button
variant="soft"
tone="neutral"
disabled={!viewport}
onClick={() => scrollTo('start')}
>
顶部
</Button>
<Button
variant="soft"
tone="neutral"
disabled={!viewport}
onClick={() => scrollTo('end')}
>
底部
</Button>
<Button onClick={close}>关闭</Button>
</>
)}
>
<Button variant="outline" tone="neutral">
打开
</Button>
</Drawer>
)
}
受控
open 可受控。省略 children 时不渲染触发器,抽屉只能从外部打开。
当前:已关闭
'use client'
import { useState } from 'react'
import { Button, Drawer, Inline, Text } from '@hina-ui/react'
export default function Demo() {
const [open, setOpen] = useState(false)
return (
<Inline align="center">
<Button variant="outline" tone="neutral" onClick={() => setOpen(true)}>
从外部打开
</Button>
<Text tone="muted" size="sm">
当前:{open ? '已打开' : '已关闭'}
</Text>
<Drawer
open={open}
onOpenChange={setOpen}
title="没有触发器的抽屉"
description="它由外部的状态控制。"
renderContent={() => <Text>省略默认插槽时不渲染触发器,只能通过 open 打开。</Text>}
renderFooter={({ close }) => <Button onClick={close}>关闭</Button>}
/>
</Inline>
)
}
锁定
设置 locked 后,按 Esc 和点击遮罩都不再关闭抽屉,已显示的关闭按钮变为不可用。此时通过 open 关闭仍然有效。
'use client'
import { useState } from 'react'
import { Button, Drawer, Text } from '@hina-ui/react'
export default function Demo() {
const [open, setOpen] = useState(false)
const [saving, setSaving] = useState(false)
function save() {
setSaving(true)
setTimeout(() => {
setSaving(false)
setOpen(false)
}, 2000)
}
return (
<Drawer
open={open}
onOpenChange={setOpen}
title="编辑标签"
description="保存过程中请不要关闭这个抽屉。"
locked={saving}
renderContent={() => (
<Text>{saving ? '正在保存,两秒后自动关闭。' : '点击保存之后抽屉会锁定两秒。'}</Text>
)}
renderFooter={({ close }) => (
<>
<Button variant="soft" tone="neutral" disabled={saving} onClick={close}>
取消
</Button>
<Button loading={saving} onClick={save}>
保存
</Button>
</>
)}
>
<Button variant="outline" tone="neutral">
编辑标签
</Button>
</Drawer>
)
}
行为
- 多个浮层按打开顺序叠放,后打开的在上方;组件的挂载先后不影响叠放。关闭后保留完整退场动画,再移除浮层。
- 抽屉打开期间页面停止滚动,焦点被限制在面板内部,关闭后回到触发器。
- 按 Esc 或点击遮罩关闭抽屉,
locked会同时禁用这两种方式。 - 默认正文区域使用 ScrollArea;
renderBody的滚动由调用方控制。
无障碍
- 面板是
role="dialog",title和description分别关联到aria-labelledby和aria-describedby。 - 标题渲染为
<h2>;隐藏标题栏或提供renderBody时,保留由title属性生成的视觉隐藏标题。 - 关闭按钮带有无障碍名称,文字取自当前语言。
API
Drawer
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
title | string | — | 必填。抽屉标题 |
description | string | — | 标题下面的说明 |
side | 'start' | 'end' | 'end' | 从哪一侧滑入 |
size | 'sm' | 'md' | 'lg' | 'md' | 抽屉的宽度 |
header | boolean | true | 是否显示标题栏,隐藏时保留无障碍名称与说明 |
closable | boolean | true | 是否显示标题栏中的关闭按钮 |
locked | boolean | false | 是否禁止用户关闭 |
open | boolean | — | 是否打开,可受控 |
className | string | — | 追加到面板上的类名 |
| 属性 | 参数 | 说明 |
|---|---|---|
children | — | 触发器,省略时不渲染 |
icon | — | 标题前的装饰图标 |
titleContent | — | 标题内容,默认显示 title 属性 |
renderBody | { close } | 自定义面板内部,替换默认标题栏、正文和页脚 |
renderContent | { close } | 正文,过高时在内部滚动 |
renderFooter | { close } | 底部操作按钮 |
Ref
| 属性 | 类型 | 说明 |
|---|---|---|
viewport | HTMLElement | undefined | 正文内置 ScrollArea 的实际滚动元素,初始化完成后可用,内容卸载后清空 |