抽屉
从屏幕边缘滑入的面板。
安装
pnpm dlx shadcn@latest add @lumi-ui/drawer
将全局变量添加到你的
@theme inline 中@theme inline {
/* --- 其他变量 --- */
--drawer-bleed: 3rem;
--ease-smooth: cubic-bezier(0.32, 0.72, 0, 1);
}将以下工具类添加到你的
globals.css 中@layer utilities {
.drawer-popup {
touch-action: auto;
transition: transform 450ms var(--ease-smooth);
&[data-swiping] {
@apply select-none;
}
&[data-ending-style] {
transition-duration: calc(var(--drawer-swipe-strength) * 400ms);
}
}
/* Bottom */
.drawer-popup[data-side="bottom"] {
--drawer-bleed: 0px;
--viewport-padding: 1rem;
max-height: 80vh;
transform: translateY(var(--drawer-swipe-movement-y));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% + var(--viewport-padding) + 2px));
}
}
/* Bottom fullBleed */
.drawer-popup[data-side="bottom"][data-layout="fullBleed"] {
@apply -mb-(--drawer-bleed);
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
/* Bottom responsive */
.drawer-popup[data-side="bottom"][data-layout="responsive"] {
@media (min-width: 640px) {
margin-bottom: calc(-1 * var(--drawer-bleed));
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
}
/* Top */
.drawer-popup[data-side="top"] {
--drawer-bleed: 0px;
--viewport-padding: 1rem;
max-height: 80vh;
transform: translateY(var(--drawer-swipe-movement-y));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(-100% - var(--viewport-padding) - 2px));
}
}
/* Top fullBleed */
.drawer-popup[data-side="top"][data-layout="fullBleed"] {
@apply -mt-(--drawer-bleed);
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(-100% + var(--drawer-bleed) - 2px));
}
}
/* Top responsive */
.drawer-popup[data-side="top"][data-layout="responsive"] {
@media (min-width: 640px) {
margin-top: calc(-1 * var(--drawer-bleed));
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(-100% + var(--drawer-bleed) - 2px));
}
}
}
/* Left */
.drawer-popup[data-side="left"] {
@apply h-full w-80 sm:w-96 max-w-[calc(100vw-2rem)];
--drawer-bleed: 0px;
--viewport-padding: 1rem;
transform: translateX(var(--drawer-swipe-movement-x));
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(-100% - var(--viewport-padding) - 2px));
}
}
/* Left fullBleed */
.drawer-popup[data-side="left"][data-layout="fullBleed"] {
@apply -ml-(--drawer-bleed) w-[calc(20rem+var(--drawer-bleed))] sm:w-[calc(24rem+var(--drawer-bleed))] max-w-[100vw];
--viewport-padding: 0px;
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(-100% + var(--drawer-bleed) - 2px));
}
}
/* Left responsive */
.drawer-popup[data-side="left"][data-layout="responsive"] {
@media (min-width: 640px) {
margin-left: calc(-1 * var(--drawer-bleed));
width: calc(24rem + var(--drawer-bleed));
max-width: 100vw;
--viewport-padding: 0px;
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(-100% + var(--drawer-bleed) - 2px));
}
}
}
/* Right */
.drawer-popup[data-side="right"] {
@apply h-full w-80 sm:w-96 max-w-[calc(100vw-2rem)];
--drawer-bleed: 0px;
--viewport-padding: 1rem;
transform: translateX(var(--drawer-swipe-movement-x));
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(100% + var(--viewport-padding) + 2px));
}
}
/* Right fullBleed */
.drawer-popup[data-side="right"][data-layout="fullBleed"] {
@apply -mr-(--drawer-bleed) w-[calc(20rem+var(--drawer-bleed))] sm:w-[calc(24rem+var(--drawer-bleed))] max-w-[100vw];
--viewport-padding: 0px;
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(100% - var(--drawer-bleed) + 2px));
}
}
/* Right responsive */
.drawer-popup[data-side="right"][data-layout="responsive"] {
@media (min-width: 640px) {
margin-right: calc(-1 * var(--drawer-bleed));
width: calc(24rem + var(--drawer-bleed));
max-width: 100vw;
--viewport-padding: 0px;
&[data-ending-style],
&[data-starting-style] {
transform: translateX(calc(100% - var(--drawer-bleed) + 2px));
}
}
}
}信息
默认情况下,我们提供了四个方向的抽屉样式,每个方向都包含三种布局选项。你可以根据自己应用的需求,删除不需要的样式和对应的变体。
基本用法
import {
Drawer,
DrawerClose,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerContent,
DrawerTitle,
DrawerTrigger,
} from "@/components/ui/drawer";
export function DrawerDemo() {
return (
<Drawer>
<DrawerTrigger>打开抽屉</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>确定要执行此操作吗?</DrawerTitle>
<DrawerDescription>
此操作无法撤销。这将永久删除你的账户,并从我们的服务器中移除你的数据。
</DrawerDescription>
</DrawerHeader>
<DrawerFooter>
<DrawerClose>取消</DrawerClose>
<DrawerClose>继续</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}组件结构
<DrawerProvider>
<DrawerIndentBackground />
<DrawerIndent>
<Drawer>
<DrawerTrigger />
<DrawerSwipeArea />
<DrawerContent>
<DrawerDragHandle />
<DrawerSelectable>
<DrawerHeader>
<DrawerTitle />
<DrawerDescription />
</DrawerHeader>
<DrawerFooter>
<DrawerClose />
</DrawerFooter>
</DrawerSelectable>
</DrawerContent>
</Drawer>
</DrawerIndent>
</DrawerProvider>方向
布局变体
嵌入式
响应式
实用示例
启用抽屉内的内容选中
可滚动内容
层叠抽屉
要启用层叠效果,请为 DrawerContent 使用 Stacked 变体,并添加以下 CSS:
/*
Drawer Stacked Popup
*/
@layer utilities {
.drawer-stacked-popup {
touch-action: auto;
--drawer-bleed: 0px;
--viewport-padding: 1rem;
max-height: 80vh;
/* Stacking math */
--peek: 1rem;
--stack-progress: clamp(0, var(--drawer-swipe-progress), 1);
--stack-step: 0.05;
--stack-peek-offset: max(0px, calc((var(--nested-drawers) - var(--stack-progress)) * var(--peek)));
--scale-base: calc(max(0, 1 - (var(--nested-drawers) * var(--stack-step))));
--scale: clamp(0, calc(var(--scale-base) + (var(--stack-step) * var(--stack-progress))), 1);
--shrink: calc(1 - var(--scale));
--height: max(0px, calc(var(--drawer-frontmost-height, var(--drawer-height)) - var(--drawer-bleed)));
height: var(--drawer-height, auto);
transform-origin: 50% calc(100% - var(--drawer-bleed));
transform:
translateY(calc(var(--drawer-swipe-movement-y) - var(--stack-peek-offset) - (var(--shrink) * var(--height))))
scale(var(--scale));
transition:
transform 450ms var(--ease-smooth),
height 450ms var(--ease-smooth),
box-shadow 450ms var(--ease-smooth);
/* Shadow */
box-shadow: 0 2px 10px rgb(0 0 0 / 0.1);
/* Pseudo overlay for nested dimming */
&::after {
content: "";
@apply absolute inset-0 rounded-[inherit] bg-transparent pointer-events-none;
transition: background-color 450ms var(--ease-smooth);
}
/* Enter/exit */
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% + var(--viewport-padding) + 2px));
}
&[data-ending-style] {
box-shadow: 0 2px 10px rgb(0 0 0 / 0);
transition-duration: calc(var(--drawer-swipe-strength) * 400ms);
}
/* Swipe states */
&[data-swiping] {
transition-duration: 0s;
@apply select-none;
}
&[data-nested-drawer-swiping] {
transition-duration: 0s;
}
/* Nested drawer open */
&[data-nested-drawer-open] {
height: calc(var(--height) + var(--drawer-bleed));
overflow: hidden;
&::after {
background-color: rgb(0 0 0 / 0.05);
}
}
}
/* Stacked fullBleed */
.drawer-stacked-popup[data-layout="fullBleed"] {
@apply -mb-(--drawer-bleed);
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
/* Stacked responsive */
.drawer-stacked-popup[data-layout="responsive"] {
@media (min-width: 640px) {
margin-bottom: calc(-1 * var(--drawer-bleed));
--viewport-padding: 0px;
max-height: calc(80vh + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
}
}吸附点
要启用吸附点支持,请添加以下 CSS:
/*
Drawer Snap Popup
*/
@layer utilities {
.drawer-snap-popup {
@apply overflow-visible min-h-0 touch-none;
--drawer-bleed: 0px;
--viewport-padding: 1rem;
max-height: calc(100dvh - var(--drawer-snap-top-margin, 1rem));
/* Snap-based transform + dynamic padding */
transform: translateY(calc(var(--drawer-snap-point-offset) + var(--drawer-swipe-movement-y)));
padding-bottom: max(0px, calc(var(--drawer-snap-point-offset) + var(--drawer-swipe-movement-y)));
transition:
transform 450ms var(--ease-smooth);
/* Bleed pseudo-element below rounded corners */
&::after {
content: "";
@apply pointer-events-none absolute inset-x-0 top-full;
height: var(--drawer-bleed);
background-color: inherit;
}
&[data-swiping] {
@apply select-none;
}
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% + var(--viewport-padding) + 2px));
padding-bottom: 0;
box-shadow:
0 -16px 48px rgb(0 0 0 / 0),
0 6px 18px rgb(0 0 0 / 0);
}
&[data-ending-style] {
transition-duration: calc(var(--drawer-swipe-strength) * 400ms);
}
}
/* Snap fullBleed */
.drawer-snap-popup[data-layout="fullBleed"] {
@apply -mb-(--drawer-bleed);
--viewport-padding: 0px;
max-height: calc(100dvh - var(--drawer-snap-top-margin, 1rem) + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
/* Snap responsive */
.drawer-snap-popup[data-layout="responsive"] {
@media (min-width: 640px) {
margin-bottom: calc(-1 * var(--drawer-bleed));
--viewport-padding: 0px;
max-height: calc(100dvh - var(--drawer-snap-top-margin, 1rem) + var(--drawer-bleed));
&[data-ending-style],
&[data-starting-style] {
transform: translateY(calc(100% - var(--drawer-bleed) + 2px));
}
}
}
}抽屉缩进效果
访问 Playground 进行体验。
设置
import {
DrawerIndent,
DrawerIndentBackground,
DrawerProvider,
} from "@/components/ui/drawer";
export default function DrawerIndentPage() {
return (
<DrawerProvider>
<DrawerIndentBackground />
<DrawerIndent>
{/* 你的抽屉 */}
</DrawerIndent>
</DrawerProvider>
);
}更多信息请参阅 Base UI。
CSS
/*
Drawer Indent
*/
@layer utilities {
.drawer-indent {
--indent-progress: var(--drawer-swipe-progress);
--indent-radius: calc(1rem * (1 - var(--indent-progress)));
--indent-transition: calc(1 - clamp(0, calc(var(--drawer-swipe-progress) * 100000), 1));
@apply origin-[center_top] will-change-transform;
transform: scale(1) translateY(0);
transition:
transform 0.4s var(--ease-smooth),
border-radius 0.25s var(--ease-smooth);
transition-duration:
calc(400ms * var(--indent-transition)),
calc(250ms * var(--indent-transition));
&[data-active] {
transform:
scale(calc(0.98 + (0.02 * var(--indent-progress))))
translateY(calc(0.5rem * (1 - var(--indent-progress))));
border-top-left-radius: var(--indent-radius);
border-top-right-radius: var(--indent-radius);
}
}
}