Drawer 抽屉
用于补充内容与操作的滑出面板
用法
import { Drawer, Button } from "@lenso/ui";此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-basic (Apache-2.0).import { Button, Drawer } from "@lenso/ui";
export function Basic() { return ( <Drawer.Provider> <Drawer swipeDirection="right"> <Drawer.Trigger render={<Button variant="secondary" />}>Open Drawer</Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Header> <Drawer.Title>Drawer Title</Drawer.Title> </Drawer.Header> <Drawer.Body> <p> This is a bottom drawer built with React Aria's Modal component. It slides up from the bottom of the screen with a smooth CSS transition. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Cancel</Drawer.Close> <Drawer.Close render={<Button />}>Confirm</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
组件结构
import { Drawer, Button } from "@lenso/ui";
export default () => ( <Drawer> <Button>Open Drawer</Button> <Drawer.Backdrop> <Drawer.Content> <Drawer.Dialog> <Drawer.Handle /> {/* Optional: Drag handle */} <Drawer.CloseTrigger /> {/* Optional: Close button */} <Drawer.Header> <Drawer.Heading /> </Drawer.Header> <Drawer.Body /> <Drawer.Footer /> </Drawer.Dialog> </Drawer.Content> </Drawer.Backdrop> </Drawer>);示例
弹出位置
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-placements (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer } from "@lenso/ui";import { styles } from "./styles";
export function Placements() { const placements = ["bottom", "top", "left", "right"] as const; return ( <div {...stylex.props(styles.row)}> {placements.map((placement) => ( <Drawer.Provider key={placement}> <Drawer swipeDirection={ placement === "bottom" ? "down" : placement === "top" ? "up" : placement } > <Drawer.Trigger render={<Button variant="secondary" />}> {placement.charAt(0).toUpperCase() + placement.slice(1)} </Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> {placement === "bottom" && <Drawer.Handle />} <Drawer.Header> <Drawer.Title> {placement.charAt(0).toUpperCase() + placement.slice(1)} Drawer </Drawer.Title> </Drawer.Header> <Drawer.Body> <p> This drawer slides in from the <strong>{placement}</strong> edge of the screen. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Cancel</Drawer.Close> <Drawer.Close render={<Button />}>Done</Drawer.Close> </Drawer.Footer> {placement === "top" && <Drawer.Handle />} </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
不可关闭
在 Drawer.Backdrop 上设置 isDismissable={false} 可阻止点击外部或拖拽关闭。用户必须通过抽屉内的操作按钮交互。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-non-dismissable (Apache-2.0).import { Button, Drawer } from "@lenso/ui";
export function NonDismissable() { return ( <Drawer.Provider> <Drawer swipeDirection="down" onOpenChange={(_open, details) => { if (details.reason === "outside-press" || details.reason === "swipe") details.cancel(); }} > <Drawer.Trigger render={<Button variant="secondary" />}>Important Action</Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Header> <Drawer.Title>Confirm Action</Drawer.Title> </Drawer.Header> <Drawer.Body> <p> This drawer cannot be dismissed by clicking outside or dragging. You must use one of the buttons below. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Cancel</Drawer.Close> <Drawer.Close render={<Button />}>Confirm</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
可滚动内容
Drawer.Body 通过原生滚动自动处理溢出。拖拽关闭在 body 区域被排除,以避免与滚动冲突。
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-scrollable-content (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer } from "@lenso/ui";import { styles } from "./styles";
export function ScrollableContent() { return ( <Drawer.Provider> <Drawer swipeDirection="down"> <Drawer.Trigger render={<Button variant="secondary" />}>Terms & Conditions</Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Handle /> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title>Terms & Conditions</Drawer.Title> </Drawer.Header> <Drawer.Body> {Array.from({ length: 20 }, (_, i) => ( <p key={i} {...stylex.props(styles.paragraph)}> Paragraph {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> ))} </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Decline</Drawer.Close> <Drawer.Close render={<Button />}>Accept</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
受控模式
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-controlled (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer, useOverlayState } from "@lenso/ui";import React from "react";import { styles } from "./styles";
export function Controlled() { const [isOpen, setIsOpen] = React.useState(false); const state = useOverlayState(); return ( <div {...stylex.props(styles.controlled)}> <div {...stylex.props(styles.section)}> <h3 {...stylex.props(styles.heading)}>With React.useState()</h3> <p {...stylex.props(styles.explanation)}> Control the drawer using React's <code>useState</code> hook for simple state management. </p> <Drawer.Provider> <Drawer open={isOpen} onOpenChange={setIsOpen} swipeDirection="right"> <div {...stylex.props(styles.stateCard)}> <p {...stylex.props(styles.status)}> Status:{" "} <span {...stylex.props(styles.statusValue)}>{isOpen ? "open" : "closed"}</span> </p> <div {...stylex.props(styles.actions)}> <Drawer.Trigger render={<Button size="sm" variant="secondary" />}> Open Drawer </Drawer.Trigger> <Button size="sm" variant="tertiary" onClick={() => setIsOpen(!isOpen)}> Toggle </Button> </div> </div> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title>Controlled with useState()</Drawer.Title> </Drawer.Header> <Drawer.Body> <p> This drawer is controlled by React's <code>useState</code> hook. Pass{" "} <code>isOpen</code> and <code>onOpenChange</code> props to manage the drawer state externally. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Close</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> </div> <div {...stylex.props(styles.section)}> <h3 {...stylex.props(styles.heading)}>With useOverlayState()</h3> <p {...stylex.props(styles.explanation)}> Use the <code>useOverlayState</code> hook for a cleaner API with convenient methods like{" "} <code>open()</code>, <code>close()</code>, and <code>toggle()</code>. </p> <Drawer.Provider> <Drawer open={state.isOpen} onOpenChange={state.setOpen} swipeDirection="right"> <div {...stylex.props(styles.stateCard)}> <p {...stylex.props(styles.status)}> Status:{" "} <span {...stylex.props(styles.statusValue)}> {state.isOpen ? "open" : "closed"} </span> </p> <div {...stylex.props(styles.actions)}> <Drawer.Trigger render={<Button size="sm" variant="secondary" />} onClick={state.open} > Open Drawer </Drawer.Trigger> <Button size="sm" variant="tertiary" onClick={state.toggle}> Toggle </Button> </div> </div> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title>Controlled with useOverlayState()</Drawer.Title> </Drawer.Header> <Drawer.Body> <p> The <code>useOverlayState</code> hook provides dedicated methods for common operations. No need to manually create callbacks—just use{" "} <code>state.open()</code>, <code>state.close()</code>, or{" "} <code>state.toggle()</code>. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Close</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> </div> </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
搭配表单
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-with-form (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer, Input, Label, TextField } from "@lenso/ui";import { styles } from "./styles";
export function WithForm() { return ( <Drawer.Provider> <Drawer swipeDirection="right"> <Drawer.Trigger render={<Button variant="secondary" />}>Edit Profile</Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title>Edit Profile</Drawer.Title> </Drawer.Header> <Drawer.Body> <form {...stylex.props(styles.form)}> <TextField fullWidth name="name"> <Label>Name</Label> <Input type="text" placeholder="Enter your name" variant="secondary" /> </TextField> <TextField fullWidth name="email"> <Label>Email</Label> <Input type="email" placeholder="Enter your email" variant="secondary" /> </TextField> <TextField fullWidth name="bio"> <Label>Bio</Label> <Input placeholder="Tell us about yourself" variant="secondary" /> </TextField> </form> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Cancel</Drawer.Close> <Drawer.Close render={<Button />}>Save Changes</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
导航抽屉
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-navigation (Apache-2.0).import type { ComponentType, SVGProps } from "react";import { Bars, Bell, Envelope, Gear, House, Magnifier, Person } from "@gravity-ui/icons";import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer } from "@lenso/ui";import { styles } from "./styles";
export function Navigation() { const navItems: { icon: ComponentType<SVGProps<SVGSVGElement>>; label: string }[] = [ { icon: House, label: "Home" }, { icon: Magnifier, label: "Search" }, { icon: Bell, label: "Notifications" }, { icon: Envelope, label: "Messages" }, { icon: Person, label: "Profile" }, { icon: Gear, label: "Settings" }, ]; return ( <Drawer.Provider> <Drawer swipeDirection="left"> <Drawer.Trigger render={<Button variant="secondary" />}> <Button.Icon> <Bars /> </Button.Icon> Menu </Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title>Navigation</Drawer.Title> </Drawer.Header> <Drawer.Body> <nav {...stylex.props(styles.navigation)}> {navItems.map((item) => ( <button key={item.label} {...stylex.props(styles.navButton)} type="button"> <item.icon {...stylex.props(styles.icon)} /> {item.label} </button> ))} </nav> </Drawer.Body> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
遮罩变体
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-backdrop-variants (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, CloseIcon, Drawer } from "@lenso/ui";import { styles } from "./styles";
export function BackdropVariants() { const variants = ["opaque", "blur", "transparent"] as const; return ( <div {...stylex.props(styles.row)}> {variants.map((variant) => ( <Drawer.Provider key={variant}> <Drawer swipeDirection="down"> <Drawer.Trigger render={<Button variant="secondary" />}> {variant.charAt(0).toUpperCase() + variant.slice(1)} </Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop variant={variant} /> <Drawer.Viewport> <Drawer.Popup> <Drawer.Content> <Drawer.Handle /> <Drawer.Close aria-label="Close drawer" render={ <Button isIconOnly size="sm" variant="tertiary" xstyle={styles.cornerClose} /> } > <CloseIcon /> </Drawer.Close> <Drawer.Header> <Drawer.Title> Backdrop: {variant.charAt(0).toUpperCase() + variant.slice(1)} </Drawer.Title> </Drawer.Header> <Drawer.Body> <p> This drawer uses the <code>{variant}</code> backdrop variant. </p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button xstyle={styles.fullWidth} />}> Close </Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> ))} </div> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
自定义样式
Tailwind CSS
此预览复用英文版适配,不代表中文源示例已完成本地实现。
"use client";
// Adapted from HeroUI v3.2.6 drawer-custom-styles (Apache-2.0).import * as stylex from "@stylexjs/stylex";import { Button, Drawer } from "@lenso/ui";import { styles } from "./styles";
export function CustomStyles() { return ( <Drawer.Provider> <Drawer swipeDirection="right"> <Drawer.Trigger render={<Button variant="secondary" />}>Open filters</Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop variant="blur" /> <Drawer.Viewport> <Drawer.Popup xstyle={styles.filters}> <Drawer.Content> <Drawer.Header> <Drawer.Title>Filters</Drawer.Title> </Drawer.Header> <Drawer.Body> <p {...stylex.props(styles.muted)}>Narrow results by status, owner, or date.</p> </Drawer.Body> <Drawer.Footer> <Drawer.Close render={<Button variant="secondary" />}>Cancel</Drawer.Close> <Drawer.Close render={<Button />}>Apply</Drawer.Close> </Drawer.Footer> </Drawer.Content> </Drawer.Popup> </Drawer.Viewport> </Drawer.Portal> </Drawer> </Drawer.Provider> );}Local adaptation source above. Derived from HeroUI v3.2.6 source.
全局 CSS
可使用 @layer components 指令自定义 Drawer 组件类。
了解更多。
@layer components { .drawer__backdrop { @apply bg-gradient-to-br from-black/50 to-black/70; }
.drawer__dialog { @apply rounded-2xl border border-white/10 shadow-2xl; }
.drawer__header { @apply text-center; }
.drawer__close-trigger { @apply rounded-full bg-white/10 hover:bg-white/20; }}样式参考
HeroUI 遵循 BEM 方法论,确保组件变体与状态可复用且易于自定义。
CSS 类
Drawer 组件使用以下 CSS 类(查看源码样式):
基础类 [!toc]
.drawer__trigger- 打开抽屉的触发元素.drawer__backdrop- 抽屉背后的遮罩层.drawer__content- 抽屉面板的定位包裹层.drawer__dialog- 抽屉面板本身.drawer__header- 标题区域.drawer__heading- 主标题文本.drawer__body- 主可滚动内容区域.drawer__footer- 操作按钮区域.drawer__handle- 视觉拖拽手柄指示器.drawer__close-trigger- 关闭按钮元素
遮罩变体 [!toc]
.drawer__backdrop--opaque- 不透明彩色遮罩(默认).drawer__backdrop--blur- 模糊遮罩,玻璃效果.drawer__backdrop--transparent- 透明遮罩(无覆盖层)
位置变体 [!toc]
.drawer__content--bottom- 从底部边缘滑入(默认).drawer__content--top- 从顶部边缘滑入.drawer__content--left- 从左侧边缘滑入.drawer__content--right- 从右侧边缘滑入
对话框变体 [!toc]
.drawer__dialog--top- 从顶部边缘滑入.drawer__dialog--bottom- 从底部边缘滑入.drawer__dialog--left- 从左侧边缘滑入.drawer__dialog--right- 从右侧边缘滑入
交互状态
组件支持以下交互状态:
- Focus:
:focus-visible或[data-focus-visible="true"]- 应用于触发器与关闭按钮 - Hover:
:hover或[data-hovered="true"]- 悬停时应用于关闭按钮 - Active:
:active或[data-pressed="true"]- 按下时应用于触发器与关闭按钮 - Entering:
[data-entering]- 抽屉打开动画期间 - Exiting:
[data-exiting]- 抽屉关闭动画期间 - Placement:
[data-placement="*"]- 根据抽屉位置(top、bottom、left、right)应用
API 参考
Drawer
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 触发器与 backdrop 元素 |
state | UseOverlayStateReturn | - | 受控 overlay 状态 |
Drawer.Trigger
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 自定义触发器内容 |
className | string | - | CSS 类 |
Drawer.Backdrop
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
variant | "opaque" | "blur" | "transparent" | "opaque" | 遮罩覆盖层样式 |
isDismissable | boolean | true | 点击 backdrop 关闭 |
isKeyboardDismissDisabled | boolean | false | 禁用 ESC 关闭 |
isOpen | boolean | - | 受控打开状态 |
onOpenChange | (isOpen: boolean) => void | - | 打开状态变化处理函数 |
className | string | (values) => string | - | Backdrop CSS 类 |
Drawer.Content
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
placement | "top" | "bottom" | "left" | "right" | "bottom" | 抽屉滑入的边缘 |
className | string | (values) => string | - | Content CSS 类 |
Drawer.Dialog
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | Dialog 内容 |
className | string | - | CSS 类 |
role | string | "dialog" | ARIA role |
aria-label | string | - | 无障碍标签 |
aria-labelledby | string | - | 标签元素 ID |
Drawer.Header
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | Header 内容 |
className | string | - | CSS 类 |
Drawer.Heading
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 标题文本 |
className | string | - | CSS 类 |
Drawer.Body
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | Body 内容 |
className | string | - | CSS 类 |
Drawer.Footer
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | Footer 内容 |
className | string | - | CSS 类 |
Drawer.Handle
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
className | string | - | CSS 类 |
Drawer.CloseTrigger
| Prop | 类型 | 默认值 | 描述 |
|---|---|---|---|
children | ReactNode | - | 自定义关闭按钮 |
className | string | (values) => string | - | CSS 类 |
useOverlayState Hook
import { useOverlayState } from "@lenso/ui";
const state = useOverlayState({ defaultOpen: false, onOpenChange: (isOpen) => console.log(isOpen),});
state.isOpen; // Current statestate.open(); // Open drawerstate.close(); // Close drawerstate.toggle(); // Toggle statestate.setOpen(); // Set state directly无障碍
- 焦点陷阱:打开时焦点锁定在抽屉内
- 键盘:
ESC关闭(可关闭时),Tab循环元素 - 屏幕阅读器:通过 React Aria 提供正确的 ARIA 属性
- 滚动锁定:打开时禁用 body 滚动
- 拖拽关闭:在 handle、header 和 footer 区域支持指针拖拽手势