Drawer
לוח שמחליק פנימה מקצה המסך.
- דפדפנים
- Chrome 123+Firefox 121+Safari 17.5+
- שכבה
- 0
- גודל חבילה דחוסה
- 3.95 kB
- מגבלת פריסה כפויה
- 0 ms
- מגבלת Axe
- 0
- תואם SSR
- כן
- מבוסס על
- elements/dialog
- חוזה מפורסם
- סביבת בדיקות
מגרש ניסויים
import { DrawerPanel } from "@gunkin/uiify";
export default function Example() {
return (
<DrawerPanel
id="settings-drawer"
side="right"
trigger="Open settings"
title="Settings"
description="Adjust your preferences."
slotProps={{
trigger: { className: "" },
content: { className: "" },
grabber: { className: "" },
header: { className: "" },
title: { className: "" },
description: { className: "" },
close: { className: "" },
body: { className: "" },
footer: { className: "" },
}}
/>
);
}@layer components {
[data-uiify-drawer][data-part="content"] {
--uiify-drawer-inline-size: 32rem;
margin: 0;
inline-size: 100%;
max-inline-size: 100%;
max-block-size: 100dvh;
padding: var(--space-xl);
border: var(--border-width) solid var(--border);
background: var(--popover);
color: var(--popover-foreground);
box-shadow: var(--shadow-modal);
flex-direction: column;
gap: var(--space-md);
}
[data-uiify-drawer][data-part="content"][open] {
display: flex;
}
[data-uiify-drawer][data-size="sm"] {
--uiify-drawer-inline-size: 24rem;
}
[data-uiify-drawer][data-size="lg"] {
--uiify-drawer-inline-size: 40rem;
}
[data-uiify-drawer][data-size="full"] {
--uiify-drawer-inline-size: 100%;
}
[data-uiify-drawer][data-part="content"]::backdrop {
background: rgb(0 0 0 / 45%);
}
[data-uiify-drawer][data-backdrop="transparent"]::backdrop {
background: transparent;
}
[data-uiify-drawer][data-part="content"][data-side="bottom"] {
margin-block-start: auto;
block-size: min(var(--uiify-drawer-inline-size), 80dvh);
max-block-size: 80dvh;
border-start-start-radius: var(--radius-xl);
border-start-end-radius: var(--radius-xl);
transform: var(--uiify-drawer-transform, none);
}
[data-uiify-drawer][data-part="content"][data-side="top"] {
margin-block-end: auto;
block-size: min(var(--uiify-drawer-inline-size), 80dvh);
max-block-size: 80dvh;
border-end-start-radius: var(--radius-xl);
border-end-end-radius: var(--radius-xl);
transform: var(--uiify-drawer-transform, none);
}
[data-uiify-drawer][data-part="content"]:is([data-side="left"], [data-side="right"]) {
inline-size: min(var(--uiify-drawer-inline-size), 100%);
block-size: 100dvh;
transform: var(--uiify-drawer-transform, none);
}
[data-uiify-drawer][data-part="content"][data-side="left"] {
margin-inline-end: auto;
}
[data-uiify-drawer][data-part="content"][data-side="right"] {
margin-inline-start: auto;
border-start-end-radius: 0;
border-end-end-radius: 0;
}
:dir(rtl) [data-uiify-drawer][data-part="content"][data-side="left"] {
margin-inline: auto 0;
}
:dir(rtl) [data-uiify-drawer][data-part="content"][data-side="right"] {
margin-inline: 0 auto;
}
[data-uiify-drawer][data-part="content"][data-size="full"] {
inline-size: 100dvi;
max-inline-size: 100dvi;
block-size: 100dvh;
max-block-size: 100dvh;
}
[data-uiify-drawer] [data-part="header"] {
flex: none;
padding-inline-end: var(--control-md);
}
[data-uiify-drawer] [data-part="body"] {
min-block-size: 0;
overflow: auto;
overscroll-behavior: contain;
touch-action: pan-y;
}
[data-uiify-drawer] [data-part="footer"] {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: var(--space-sm);
}
[data-uiify-drawer] [data-part="close"] {
display: inline-grid;
place-items: center;
}
[data-uiify-drawer] [data-part="header"] > [data-part="close"] {
position: absolute;
inset-block-start: 10px;
inset-inline-end: 10px;
}
/* Close glyph: two crossed bars on the ghost Button's free pseudo-elements (see modal.css). */
[data-uiify-drawer] [data-part="close"]:empty {
&::before,
&::after {
all: unset;
content: "";
grid-area: 1 / 1;
inline-size: 0.875rem;
block-size: 2px;
border-radius: 1px;
background-color: currentcolor;
}
&::before {
rotate: 45deg;
}
&::after {
rotate: -45deg;
}
}
[data-uiify-drawer] :is([data-part="grabber"], [data-part="handle"]) {
display: none;
flex: none;
block-size: 1.5rem;
touch-action: none;
}
[data-uiify-drawer] :is([data-part="grabber"], [data-part="handle"])::after {
content: "";
display: block;
inline-size: 2.25rem;
block-size: 0.25rem;
margin-block: 0.5rem 0;
margin-inline: auto;
border-radius: 999px;
background: var(--border);
}
[data-uiify-drawer][data-snap-enabled] [data-part="handle"] {
display: block;
}
@media (width < 40rem) {
[data-uiify-drawer][data-part="content"][data-side] {
inline-size: 100dvi;
max-inline-size: 100dvi;
block-size: auto;
min-block-size: 50dvh;
max-block-size: 80dvh;
margin-block: auto 0;
margin-inline: 0;
border-start-start-radius: var(--radius-xl);
border-start-end-radius: var(--radius-xl);
border-end-start-radius: 0;
border-end-end-radius: 0;
transform: var(--uiify-drawer-transform, none);
translate: 0 var(--uiify-sheet-offset, 0);
padding-block-end: calc(var(--space-xl) + env(safe-area-inset-bottom));
}
[data-uiify-drawer][data-part="content"][data-size="full"] {
block-size: 100dvh;
max-block-size: 100dvh;
border-radius: 0;
}
[data-uiify-drawer] :is([data-part="grabber"], [data-part="handle"]) {
display: block;
}
[data-uiify-drawer] [data-part="header"] {
touch-action: none;
}
[data-uiify-drawer][data-dragging] {
transition: none;
}
}
@media (prefers-reduced-motion: no-preference) {
[data-uiify-drawer][data-part="content"] {
transition:
opacity 180ms ease,
translate 220ms ease,
transform 220ms ease,
display 220ms allow-discrete,
overlay 220ms allow-discrete;
}
[data-uiify-drawer][data-part="content"]:not([open]) {
opacity: 0;
}
@starting-style {
[data-uiify-drawer][data-part="content"][open] {
opacity: 0;
}
}
}
}import { DrawerPanel } from "@gunkin/uiify";
export default function Example() {
return (
<DrawerPanel
id="settings-drawer"
side="right"
trigger="Open settings"
title="Settings"
description="Adjust your preferences."
slotProps={{
trigger: {
className:
"relative inline-flex min-h-10 min-w-10 items-center justify-center rounded-full " +
"border-[length:var(--border-width)] border-solid border-transparent " +
"bg-foreground px-5 py-0 text-background text-base font-medium leading-6 " +
"no-underline cursor-pointer [transition:var(--transition-colors)]",
},
content: {
className:
"[&[data-positioning=native]]:[--uiify-anchor-offset:var(--space-sm)] " +
"[&[popover]:popover-open:not([data-positioning=native])]:[margin:revert] " +
"[--uiify-drawer-inline-size:32rem] m-0 w-full max-w-full max-h-dvh p-6 border " +
"border-border bg-popover text-popover-foreground shadow-modal flex-col gap-md " +
"rounded-xl data-[size=sm]:[--uiify-drawer-inline-size:24rem] " +
"data-[size=lg]:[--uiify-drawer-inline-size:40rem] " +
"data-[size=full]:[--uiify-drawer-inline-size:100%] [&[open]]:flex " +
"data-[side=right]:ms-auto data-[side=left]:me-auto data-[side=top]:mb-auto " +
"data-[side=bottom]:mt-auto " +
"data-[side=bottom]:h-[min(var(--uiify-drawer-inline-size),_80dvh)] " +
"data-[side=top]:h-[min(var(--uiify-drawer-inline-size),_80dvh)] " +
"data-[side=left]:w-[min(var(--uiify-drawer-inline-size),_100%)] " +
"data-[side=right]:w-[min(var(--uiify-drawer-inline-size),_100%)] " +
"[&:is([data-side=left],[data-side=right])]:[block-size:100dvh] " +
"data-[side=right]:[border-start-end-radius:0] " +
"data-[side=right]:[border-end-end-radius:0] " +
"data-[side=left]:[border-start-start-radius:0] " +
"data-[side=left]:[border-end-start-radius:0] " +
"data-[size=full]:[inline-size:100dvi] data-[size=full]:[block-size:100dvh] " +
"[&::backdrop]:[background:rgb(0_0_0_/_45%)] " +
"data-[backdrop=transparent]:[&::backdrop]:bg-transparent " +
"max-[40rem]:[inline-size:100dvi] max-[40rem]:[block-size:auto] " +
"max-[40rem]:[min-block-size:50dvh] max-[40rem]:[max-block-size:80dvh] " +
"max-[40rem]:[margin-block:auto_0] max-[40rem]:[margin-inline:0] " +
"max-[40rem]:[border-start-start-radius:var(--radius-xl)] " +
"max-[40rem]:[border-start-end-radius:var(--radius-xl)] " +
"max-[40rem]:[border-end-start-radius:0] max-[40rem]:[border-end-end-radius:0] " +
"max-[40rem]:[padding-block-end:calc(var(--space-xl)_+_env(safe-area-inset-bottom))]",
},
grabber: {
className:
"hidden flex-none [block-size:1.5rem] [touch-action:none] after:content-[''] " +
"after:block after:w-9 after:h-1 after:[margin-block:0.5rem_0] after:mx-auto " +
"after:rounded-full after:bg-border max-[40rem]:block",
},
header: { className: "flex-none pe-11 max-[40rem]:[touch-action:none]" },
title: { className: "m-0 text-[length:var(--text-xl)]" },
description: { className: "m-0 text-muted-foreground text-sm" },
close: {
className:
"absolute [inset-block-start:10px] [inset-inline-end:10px] inline-grid " +
"place-items-center empty:before:[all:unset] empty:before:content-[''] " +
"empty:before:[grid-area:1/1] empty:before:[inline-size:0.875rem] " +
"empty:before:[block-size:2px] empty:before:[border-radius:1px] " +
"empty:before:bg-current empty:before:[rotate:45deg] empty:after:[all:unset] " +
"empty:after:content-[''] empty:after:[grid-area:1/1] " +
"empty:after:[inline-size:0.875rem] empty:after:[block-size:2px] " +
"empty:after:[border-radius:1px] empty:after:bg-current " +
"empty:after:[rotate:-45deg]",
},
body: { className: "min-h-0 overflow-auto overscroll-contain [touch-action:pan-y]" },
footer: { className: "flex flex-wrap justify-end gap-sm" },
}}
/>
);
}API
DrawerPanel
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
size | "sm" | "md" | "lg" | "full" | "md" | רוחבים במחשב: 24rem, 32rem, 40rem או הרוחב הזמין. בנייד כל גודל הופך ללוח תחתון אנכי. |
dismiss | "any" | "closerequest" | "none" | "any" | מדיניות סגירה מובנית. Any מאפשר לחיצה על הרקע, Escape והחלקה מטה בנייד; none מחייב פקד סגירה מפורש. |
backdrop | "opaque" | "transparent" | "opaque" | מראה הרקע; אינו משנה את המשמעות המודאלית. |
footer | ReactNode | — | הפעולות שאחרי הגוף הניתן לגלילה. |
id* | string | — | מזהה ייחודי לתיבת הדו-שיח שאליו מכוונים כפתורי הפתיחה והסגירה, כדי שהמגירה תיפתח ותיסגר לפני ההידרציה. |
trigger* | ReactNode | — | תוכן הכפתור שפותח את המגירה. |
title* | ReactNode | — | כותרת המוצגת בתוך המגירה ומשמשת כשמה הנגיש. |
description | ReactNode | — | טקסט משלים המוצג מתחת לכותרת ומקושר באמצעות aria-describedby. |
children | ReactNode | — | תוכן הגוף המוצב בין התיאור לכפתור הסגירה. |
side | "top" | "right" | "bottom" | "left" | "bottom" | הקצה שממנו המגירה מחליקה פנימה. |
closeLabel | string | "Close" | תווית נגישה לאייקון הסגירה העגול. |
className | string | — | המחלקה המוחלת על אלמנט השורש של DrawerPanel. |
slotProps | DrawerSlotProps | — | מאפיינים מובנים לחלקים ש-DrawerPanel יוצר; השורש שלו מקבל מאפיינים מובנים ישירות. |
Drawer.Root (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
open | boolean | — | מצב פתיחה מבוקר. |
defaultOpen | boolean | — | מצב פתיחה התחלתי לא מבוקר. |
onOpenChange | DialogChangeHandler | — | נקראת עם מצב הפתיחה הבא ופרטי הגורם לשינוי. |
side | "top" | "right" | "bottom" | "left" | "bottom" | הקצה שממנו המגירה מחליקה פנימה, הקובע גם את ציר הגרירה. |
modal | boolean | true | פתיחה באמצעות showModal() כך ששאר העמוד אינו אינטראקטיבי מאחורי רקע חוצץ. |
dismissible | boolean | true | מאפשר גרירה מעבר לנקודת ההצמדה האחרונה לסגירת המגירה. |
snapPoints | readonly number[] | — | חלקים יחסיים מגודל המגירה שבהם הלוח מתייצב בעת גרירה. |
activeSnapPoint | number | — | נקודת הצמדה מבוקרת, כאחד הערכים ב-snapPoints. |
defaultActiveSnapPoint | number | — | נקודת הצמדה התחלתית לא מבוקרת. |
onSnapChange | (snap: number) => void | — | נקראת עם נקודת ההצמדה שבה המגירה התייצבה. |
children | ReactNode | — | החלקים Trigger, Content והחלקים הפנימיים. |
Drawer.Trigger (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על כפתור הפתיחה. |
onClick | (event: MouseEvent<HTMLButtonElement>) => void | — | נקראת בלחיצה על כפתור הפתיחה, לפני שהמגירה נפתחת. |
ref | Ref<HTMLButtonElement> | — | ref לכפתור הפתיחה, שמקבל שוב מיקוד כשהמגירה נסגרת. |
children | ReactNode | — | תוכן כפתור הפתיחה. |
Drawer.Content (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על אלמנט תיבת הדו-שיח. |
style | CSSProperties | — | סגנון מוטבע המתמזג עם טרנספורמציית הגרירה שהמגירה מחילה. |
onCancel | ReactEventHandler<HTMLDialogElement> | — | נקראת באירוע cancel המובנה של תיבת הדו-שיח, כגון לחיצה על Escape. |
onClose | ReactEventHandler<HTMLDialogElement> | — | נקראת באירוע close המובנה של תיבת הדו-שיח. |
ref | Ref<HTMLDialogElement> | — | ref לאלמנט תיבת הדו-שיח. |
children | ReactNode | — | תוכן המגירה. |
Drawer.Title (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על אלמנט הכותרת. |
children | ReactNode | — | תוכן הכותרת, שהופך לשם הנגיש של המגירה. |
Drawer.Description (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על אלמנט התיאור. |
children | ReactNode | — | תוכן התיאור, המקושר באמצעות aria-describedby. |
Drawer.Close (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על כפתור הסגירה. |
onClick | (event: MouseEvent<HTMLButtonElement>) => void | — | נקראת בלחיצה על כפתור הסגירה, לפני שהמגירה נסגרת. |
children | ReactNode | — | תוכן כפתור הסגירה. |
Drawer.Handle (client)
| מאפיין | טיפוס | ברירת מחדל | תיאור |
|---|---|---|---|
className | string | — | המחלקה המוחלת על ידית הגרירה. |
onPointerDown | (event: PointerEvent<HTMLDivElement>) => void | — | נקראת כשהגרירה מתחילה על הידית. |
onPointerMove | (event: PointerEvent<HTMLDivElement>) => void | — | נקראת כשהמצביע נע במהלך גרירה. |
onPointerUp | (event: PointerEvent<HTMLDivElement>) => void | — | נקראת כשהגרירה מסתיימת והמגירה מתייצבת בנקודת הצמדה. |
onPointerCancel | (event: PointerEvent<HTMLDivElement>) => void | — | נקראת כשהדפדפן מבטל את הגרירה. |
מקלדת
| מקש | פעולה |
|---|---|
| Enter / Space | פתיחת תיבת הדו-שיח מהמפעיל שלה או הפעלת Close |
| Escape | בקשת סגירה, אלא אם dismiss הוא none |
| Tab / Shift+Tab | מעבר בתוך תיבת הדו-שיח המודאלית |
נגישות
DrawerPanel משתמש בתיבת דו-שיח עם מזהי יעד, כך שפתיחה, סגירה, כליאת מיקוד והחזרת מיקוד פועלות לפני הידרציה; dismiss שולט ברקע וב-Escape. הלוחות זמינים ברוחבים sm, md, lg ו-full והופכים ליריעות תחתונות בטלפונים. ההחלקה מתחילה בידית או בכותרת, כך שהגוף עדיין נגלל. ה-API בצד הלקוח מוסיף מצב מבוקר ונקודות עצירה.
נקודות חיבור לעיצוב
data-uiify-drawerdata-partdata-sidedata-sizedata-backdropdata-statedata-dragging--uiify-drawer-inline-size--uiify-drawer-transform--uiify-sheet-offset