Drawer
A panel that slides in from the edge of the screen, built on top of Dialog primitives. Supports swipe-to-dismiss on touch devices, four side positions, and both modal and non-modal modes.
Side drawer
A drawer that slides in from the right side of the screen.
View source
import * as Drawer from "@danielfrg/solid-ui/drawer"
import styles from "./index.module.css"
export function DemoDrawerHero() {
return (
<Drawer.Root side="right">
<Drawer.Trigger class={styles.button}>Open drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop class={styles.backdrop} />
<Drawer.Popup class={`${styles.popup} ${styles["popup-right"]}`}>
<Drawer.Title class={styles.title}>Drawer</Drawer.Title>
<Drawer.Description class={styles.description}>
This is a drawer that slides in from the side. You can swipe to dismiss it on touch devices.
</Drawer.Description>
<div class={styles.actions}>
<Drawer.Close class={styles.button}>Close</Drawer.Close>
</div>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
)
}/* ===== Shared ===== */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
border: 1px solid var(--color-gray-300);
background-color: white;
color: var(--color-gray-900);
cursor: pointer;
outline: none;
}
.button:hover {
background-color: var(--color-gray-100);
}
.button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
.title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-gray-900);
}
.description {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
/* ===== Backdrop ===== */
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 200;
animation: backdropHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.backdrop[data-expanded] {
animation: backdropShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* ===== Popup (shared base) ===== */
.popup {
position: fixed;
z-index: 200;
display: flex;
flex-direction: column;
background-color: white;
outline: none;
}
.popup[data-swiping] {
animation: none !important;
transition: none !important;
}
/* ===== Right side drawer ===== */
.popup-right {
top: 0;
right: 0;
bottom: 0;
width: 20rem;
max-width: 85vw;
padding: 1.5rem;
border-left: 1px solid var(--color-gray-200);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
animation: slideRightHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-right[data-expanded] {
animation: slideRightShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
}
/* ===== Bottom sheet ===== */
.popup-bottom {
left: 0;
right: 0;
bottom: 0;
max-height: 80vh;
padding: 1rem 1.5rem 1.5rem;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
text-align: center;
transform: translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-bottom[data-expanded] {
animation: slideBottomShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateY(var(--drawer-swipe-movement-y, 0px));
}
.popup-bottom .title {
margin-top: 0.5rem;
}
.popup-bottom .actions {
justify-content: center;
}
/* ===== Non-modal variant ===== */
.popup-non-modal {
box-shadow:
-4px 0 16px -4px rgba(0, 0, 0, 0.08),
-8px 0 24px -4px rgba(0, 0, 0, 0.04);
}
/* ===== Handle (bottom sheet drag indicator) ===== */
.handle {
width: 3rem;
height: 0.25rem;
border-radius: 9999px;
background-color: var(--color-gray-300);
margin: 0 auto 0.75rem;
flex-shrink: 0;
}
/* ===== Mobile nav ===== */
.popup-mobile-nav {
left: 50%;
bottom: 0;
width: 100%;
max-width: 42rem;
height: 85vh;
max-height: 85vh;
overflow: hidden;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomCenteredHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-mobile-nav[data-expanded] {
animation: slideBottomCenteredShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
}
.panel {
position: relative;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem 1.5rem;
flex: 1;
min-height: 0;
}
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-bottom: 0.75rem;
}
.header-spacer {
width: 2.25rem;
height: 2.25rem;
}
.close-button {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
cursor: pointer;
justify-self: end;
outline: none;
}
.close-button:hover {
background-color: var(--color-gray-100);
}
.close-button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.nav-title {
margin: 0 0 0.25rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
color: var(--color-gray-900);
}
.nav-description {
margin: 0 0 1.25rem;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.scroll-area-root {
position: relative;
flex: 1;
min-height: 0;
}
.scroll-area-viewport {
width: 100%;
height: 100%;
padding-bottom: 2rem;
}
.scroll-area-viewport::-webkit-scrollbar {
display: none;
}
.scroll-area-scrollbar {
display: flex;
width: 0.25rem;
margin: 0.25rem;
justify-content: center;
border-radius: 1rem;
opacity: 0;
transition: opacity 200ms;
}
.scroll-area-root:hover .scroll-area-scrollbar,
.scroll-area-scrollbar[data-scrolling] {
opacity: 1;
transition-duration: 75ms;
}
.scroll-area-thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-400);
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.25rem;
}
.long-list {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
display: grid;
gap: 0.25rem;
}
.item {
display: flex;
}
.link {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
color: inherit;
text-decoration: none;
background-color: var(--color-gray-100);
}
.link:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ===== Keyframes ===== */
@keyframes backdropShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdropHide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideRightShow {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideRightHide {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slideBottomShow {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes slideBottomCenteredShow {
from {
transform: translateX(-50%) translateY(100%);
}
to {
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideBottomCenteredHide {
from {
transform: translateX(-50%) translateY(0);
}
to {
transform: translateX(-50%) translateY(100%);
}
}
@keyframes slideBottomHide {
from {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Bottom sheet
Use side="bottom" to create a bottom sheet drawer with a drag handle.
View source
import * as Drawer from "@danielfrg/solid-ui/drawer"
import styles from "./index.module.css"
export function DemoDrawerPosition() {
return (
<Drawer.Root side="bottom">
<Drawer.Trigger class={styles.button}>Open bottom drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop class={styles.backdrop} />
<Drawer.Popup class={`${styles.popup} ${styles["popup-bottom"]}`}>
<div class={styles.handle} />
<Drawer.Title class={styles.title}>Notifications</Drawer.Title>
<Drawer.Description class={styles.description}>You are all caught up. Good job!</Drawer.Description>
<div class={styles.actions}>
<Drawer.Close class={styles.button}>Close</Drawer.Close>
</div>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
)
}/* ===== Shared ===== */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
border: 1px solid var(--color-gray-300);
background-color: white;
color: var(--color-gray-900);
cursor: pointer;
outline: none;
}
.button:hover {
background-color: var(--color-gray-100);
}
.button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
.title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-gray-900);
}
.description {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
/* ===== Backdrop ===== */
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 200;
animation: backdropHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.backdrop[data-expanded] {
animation: backdropShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* ===== Popup (shared base) ===== */
.popup {
position: fixed;
z-index: 200;
display: flex;
flex-direction: column;
background-color: white;
outline: none;
}
.popup[data-swiping] {
animation: none !important;
transition: none !important;
}
/* ===== Right side drawer ===== */
.popup-right {
top: 0;
right: 0;
bottom: 0;
width: 20rem;
max-width: 85vw;
padding: 1.5rem;
border-left: 1px solid var(--color-gray-200);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
animation: slideRightHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-right[data-expanded] {
animation: slideRightShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
}
/* ===== Bottom sheet ===== */
.popup-bottom {
left: 0;
right: 0;
bottom: 0;
max-height: 80vh;
padding: 1rem 1.5rem 1.5rem;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
text-align: center;
transform: translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-bottom[data-expanded] {
animation: slideBottomShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateY(var(--drawer-swipe-movement-y, 0px));
}
.popup-bottom .title {
margin-top: 0.5rem;
}
.popup-bottom .actions {
justify-content: center;
}
/* ===== Non-modal variant ===== */
.popup-non-modal {
box-shadow:
-4px 0 16px -4px rgba(0, 0, 0, 0.08),
-8px 0 24px -4px rgba(0, 0, 0, 0.04);
}
/* ===== Handle (bottom sheet drag indicator) ===== */
.handle {
width: 3rem;
height: 0.25rem;
border-radius: 9999px;
background-color: var(--color-gray-300);
margin: 0 auto 0.75rem;
flex-shrink: 0;
}
/* ===== Mobile nav ===== */
.popup-mobile-nav {
left: 50%;
bottom: 0;
width: 100%;
max-width: 42rem;
height: 85vh;
max-height: 85vh;
overflow: hidden;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomCenteredHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-mobile-nav[data-expanded] {
animation: slideBottomCenteredShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
}
.panel {
position: relative;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem 1.5rem;
flex: 1;
min-height: 0;
}
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-bottom: 0.75rem;
}
.header-spacer {
width: 2.25rem;
height: 2.25rem;
}
.close-button {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
cursor: pointer;
justify-self: end;
outline: none;
}
.close-button:hover {
background-color: var(--color-gray-100);
}
.close-button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.nav-title {
margin: 0 0 0.25rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
color: var(--color-gray-900);
}
.nav-description {
margin: 0 0 1.25rem;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.scroll-area-root {
position: relative;
flex: 1;
min-height: 0;
}
.scroll-area-viewport {
width: 100%;
height: 100%;
padding-bottom: 2rem;
}
.scroll-area-viewport::-webkit-scrollbar {
display: none;
}
.scroll-area-scrollbar {
display: flex;
width: 0.25rem;
margin: 0.25rem;
justify-content: center;
border-radius: 1rem;
opacity: 0;
transition: opacity 200ms;
}
.scroll-area-root:hover .scroll-area-scrollbar,
.scroll-area-scrollbar[data-scrolling] {
opacity: 1;
transition-duration: 75ms;
}
.scroll-area-thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-400);
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.25rem;
}
.long-list {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
display: grid;
gap: 0.25rem;
}
.item {
display: flex;
}
.link {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
color: inherit;
text-decoration: none;
background-color: var(--color-gray-100);
}
.link:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ===== Keyframes ===== */
@keyframes backdropShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdropHide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideRightShow {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideRightHide {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slideBottomShow {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes slideBottomCenteredShow {
from {
transform: translateX(-50%) translateY(100%);
}
to {
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideBottomCenteredHide {
from {
transform: translateX(-50%) translateY(0);
}
to {
transform: translateX(-50%) translateY(100%);
}
}
@keyframes slideBottomHide {
from {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Non-modal
Set modal={false} to allow interaction with the rest of the page while the drawer is open. No backdrop is rendered.
View source
import * as Drawer from "@danielfrg/solid-ui/drawer"
import styles from "./index.module.css"
export function DemoDrawerNonModal() {
return (
<Drawer.Root side="right" modal={false}>
<Drawer.Trigger class={styles.button}>Open non-modal drawer</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Popup
class={`${styles.popup} ${styles["popup-right"]} ${styles["popup-non-modal"]}`}
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
>
<Drawer.Title class={styles.title}>Non-modal drawer</Drawer.Title>
<Drawer.Description class={styles.description}>
This drawer does not trap focus and allows interaction with the rest of the page. Use the close button or
swipe to dismiss it.
</Drawer.Description>
<div class={styles.actions}>
<Drawer.Close class={styles.button}>Close</Drawer.Close>
</div>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
)
}/* ===== Shared ===== */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
border: 1px solid var(--color-gray-300);
background-color: white;
color: var(--color-gray-900);
cursor: pointer;
outline: none;
}
.button:hover {
background-color: var(--color-gray-100);
}
.button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
.title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-gray-900);
}
.description {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
/* ===== Backdrop ===== */
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 200;
animation: backdropHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.backdrop[data-expanded] {
animation: backdropShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* ===== Popup (shared base) ===== */
.popup {
position: fixed;
z-index: 200;
display: flex;
flex-direction: column;
background-color: white;
outline: none;
}
.popup[data-swiping] {
animation: none !important;
transition: none !important;
}
/* ===== Right side drawer ===== */
.popup-right {
top: 0;
right: 0;
bottom: 0;
width: 20rem;
max-width: 85vw;
padding: 1.5rem;
border-left: 1px solid var(--color-gray-200);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
animation: slideRightHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-right[data-expanded] {
animation: slideRightShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
}
/* ===== Bottom sheet ===== */
.popup-bottom {
left: 0;
right: 0;
bottom: 0;
max-height: 80vh;
padding: 1rem 1.5rem 1.5rem;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
text-align: center;
transform: translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-bottom[data-expanded] {
animation: slideBottomShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateY(var(--drawer-swipe-movement-y, 0px));
}
.popup-bottom .title {
margin-top: 0.5rem;
}
.popup-bottom .actions {
justify-content: center;
}
/* ===== Non-modal variant ===== */
.popup-non-modal {
box-shadow:
-4px 0 16px -4px rgba(0, 0, 0, 0.08),
-8px 0 24px -4px rgba(0, 0, 0, 0.04);
}
/* ===== Handle (bottom sheet drag indicator) ===== */
.handle {
width: 3rem;
height: 0.25rem;
border-radius: 9999px;
background-color: var(--color-gray-300);
margin: 0 auto 0.75rem;
flex-shrink: 0;
}
/* ===== Mobile nav ===== */
.popup-mobile-nav {
left: 50%;
bottom: 0;
width: 100%;
max-width: 42rem;
height: 85vh;
max-height: 85vh;
overflow: hidden;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomCenteredHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-mobile-nav[data-expanded] {
animation: slideBottomCenteredShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
}
.panel {
position: relative;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem 1.5rem;
flex: 1;
min-height: 0;
}
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-bottom: 0.75rem;
}
.header-spacer {
width: 2.25rem;
height: 2.25rem;
}
.close-button {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
cursor: pointer;
justify-self: end;
outline: none;
}
.close-button:hover {
background-color: var(--color-gray-100);
}
.close-button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.nav-title {
margin: 0 0 0.25rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
color: var(--color-gray-900);
}
.nav-description {
margin: 0 0 1.25rem;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.scroll-area-root {
position: relative;
flex: 1;
min-height: 0;
}
.scroll-area-viewport {
width: 100%;
height: 100%;
padding-bottom: 2rem;
}
.scroll-area-viewport::-webkit-scrollbar {
display: none;
}
.scroll-area-scrollbar {
display: flex;
width: 0.25rem;
margin: 0.25rem;
justify-content: center;
border-radius: 1rem;
opacity: 0;
transition: opacity 200ms;
}
.scroll-area-root:hover .scroll-area-scrollbar,
.scroll-area-scrollbar[data-scrolling] {
opacity: 1;
transition-duration: 75ms;
}
.scroll-area-thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-400);
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.25rem;
}
.long-list {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
display: grid;
gap: 0.25rem;
}
.item {
display: flex;
}
.link {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
color: inherit;
text-decoration: none;
background-color: var(--color-gray-100);
}
.link:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ===== Keyframes ===== */
@keyframes backdropShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdropHide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideRightShow {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideRightHide {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slideBottomShow {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes slideBottomCenteredShow {
from {
transform: translateX(-50%) translateY(100%);
}
to {
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideBottomCenteredHide {
from {
transform: translateX(-50%) translateY(0);
}
to {
transform: translateX(-50%) translateY(100%);
}
}
@keyframes slideBottomHide {
from {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}Mobile navigation
A full-screen bottom sheet with a scrollable list of navigation items. Uses the ScrollArea component for a custom scrollbar.
View source
import * as Drawer from "@danielfrg/solid-ui/drawer"
import * as ScrollArea from "@danielfrg/solid-ui/scroll-area"
import styles from "./index.module.css"
const NAV_ITEMS = [
{ href: "#", label: "Overview" },
{ href: "#", label: "Components" },
{ href: "#", label: "Utilities" },
{ href: "#", label: "Releases" },
] as const
const LONG_LIST = Array.from({ length: 50 }, (_, i) => ({
href: "#",
label: `Item ${i + 1}`,
}))
export function DemoDrawerMobileNav() {
return (
<Drawer.Root side="bottom">
<Drawer.Trigger class={styles.button}>Open mobile menu</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop class={styles.backdrop} />
<Drawer.Popup class={`${styles.popup} ${styles["popup-mobile-nav"]}`}>
<nav aria-label="Navigation" class={styles.panel}>
<div class={styles.header}>
<div aria-hidden class={styles["header-spacer"]} />
<div class={styles.handle} />
<Drawer.Close aria-label="Close menu" class={styles["close-button"]}>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<path
d="M0.75 0.75L6 6M11.25 11.25L6 6M6 6L0.75 11.25M6 6L11.25 0.75"
stroke="currentcolor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
</Drawer.Close>
</div>
<Drawer.Title class={styles["nav-title"]}>Menu</Drawer.Title>
<Drawer.Description class={styles["nav-description"]}>
Scroll the long list. Flick down from the top to dismiss.
</Drawer.Description>
<ScrollArea.Root class={styles["scroll-area-root"]}>
<ScrollArea.Viewport class={styles["scroll-area-viewport"]}>
<ul class={styles.list}>
{NAV_ITEMS.map((item) => (
<li class={styles.item}>
<a class={styles.link} href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
<ul class={styles["long-list"]} aria-label="Long list">
{LONG_LIST.map((item) => (
<li class={styles.item}>
<a class={styles.link} href={item.href}>
{item.label}
</a>
</li>
))}
</ul>
</ScrollArea.Viewport>
<ScrollArea.Scrollbar class={styles["scroll-area-scrollbar"]}>
<ScrollArea.Thumb class={styles["scroll-area-thumb"]} />
</ScrollArea.Scrollbar>
</ScrollArea.Root>
</nav>
</Drawer.Popup>
</Drawer.Portal>
</Drawer.Root>
)
}/* ===== Shared ===== */
.button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.5rem 1rem;
font-size: 0.875rem;
font-weight: 500;
border-radius: 0.375rem;
border: 1px solid var(--color-gray-300);
background-color: white;
color: var(--color-gray-900);
cursor: pointer;
outline: none;
}
.button:hover {
background-color: var(--color-gray-100);
}
.button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: 2px;
}
.title {
margin: 0;
font-size: 1.125rem;
font-weight: 600;
color: var(--color-gray-900);
}
.description {
margin: 0.5rem 0 0;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.actions {
display: flex;
gap: 0.5rem;
margin-top: 1.5rem;
}
/* ===== Backdrop ===== */
.backdrop {
position: fixed;
inset: 0;
background-color: rgba(0, 0, 0, 0.2);
z-index: 200;
animation: backdropHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.backdrop[data-expanded] {
animation: backdropShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
}
/* ===== Popup (shared base) ===== */
.popup {
position: fixed;
z-index: 200;
display: flex;
flex-direction: column;
background-color: white;
outline: none;
}
.popup[data-swiping] {
animation: none !important;
transition: none !important;
}
/* ===== Right side drawer ===== */
.popup-right {
top: 0;
right: 0;
bottom: 0;
width: 20rem;
max-width: 85vw;
padding: 1.5rem;
border-left: 1px solid var(--color-gray-200);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
animation: slideRightHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-right[data-expanded] {
animation: slideRightShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(var(--drawer-swipe-movement-x, 0px));
}
/* ===== Bottom sheet ===== */
.popup-bottom {
left: 0;
right: 0;
bottom: 0;
max-height: 80vh;
padding: 1rem 1.5rem 1.5rem;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
text-align: center;
transform: translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-bottom[data-expanded] {
animation: slideBottomShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateY(var(--drawer-swipe-movement-y, 0px));
}
.popup-bottom .title {
margin-top: 0.5rem;
}
.popup-bottom .actions {
justify-content: center;
}
/* ===== Non-modal variant ===== */
.popup-non-modal {
box-shadow:
-4px 0 16px -4px rgba(0, 0, 0, 0.08),
-8px 0 24px -4px rgba(0, 0, 0, 0.04);
}
/* ===== Handle (bottom sheet drag indicator) ===== */
.handle {
width: 3rem;
height: 0.25rem;
border-radius: 9999px;
background-color: var(--color-gray-300);
margin: 0 auto 0.75rem;
flex-shrink: 0;
}
/* ===== Mobile nav ===== */
.popup-mobile-nav {
left: 50%;
bottom: 0;
width: 100%;
max-width: 42rem;
height: 85vh;
max-height: 85vh;
overflow: hidden;
border-radius: 1rem 1rem 0 0;
border-top: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
animation: slideBottomCenteredHide 300ms cubic-bezier(0.32, 0.72, 0, 1) forwards;
}
.popup-mobile-nav[data-expanded] {
animation: slideBottomCenteredShow 300ms cubic-bezier(0.32, 0.72, 0, 1);
transform: translateX(-50%) translateY(var(--drawer-swipe-movement-y, 0px));
}
.panel {
position: relative;
display: flex;
flex-direction: column;
padding: 1rem 1.5rem 1.5rem;
flex: 1;
min-height: 0;
}
.header {
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
margin-bottom: 0.75rem;
}
.header-spacer {
width: 2.25rem;
height: 2.25rem;
}
.close-button {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
border-radius: 9999px;
border: 1px solid var(--color-gray-200);
background-color: var(--color-gray-50);
color: var(--color-gray-900);
cursor: pointer;
justify-self: end;
outline: none;
}
.close-button:hover {
background-color: var(--color-gray-100);
}
.close-button:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
.nav-title {
margin: 0 0 0.25rem;
font-size: 1.125rem;
line-height: 1.75rem;
font-weight: 500;
color: var(--color-gray-900);
}
.nav-description {
margin: 0 0 1.25rem;
font-size: 0.875rem;
line-height: 1.5;
color: var(--color-gray-600);
}
.scroll-area-root {
position: relative;
flex: 1;
min-height: 0;
}
.scroll-area-viewport {
width: 100%;
height: 100%;
padding-bottom: 2rem;
}
.scroll-area-viewport::-webkit-scrollbar {
display: none;
}
.scroll-area-scrollbar {
display: flex;
width: 0.25rem;
margin: 0.25rem;
justify-content: center;
border-radius: 1rem;
opacity: 0;
transition: opacity 200ms;
}
.scroll-area-root:hover .scroll-area-scrollbar,
.scroll-area-scrollbar[data-scrolling] {
opacity: 1;
transition-duration: 75ms;
}
.scroll-area-thumb {
width: 100%;
border-radius: inherit;
background-color: var(--color-gray-400);
}
.list {
list-style: none;
padding: 0;
margin: 0;
display: grid;
gap: 0.25rem;
}
.long-list {
list-style: none;
padding: 0;
margin: 1.5rem 0 0;
display: grid;
gap: 0.25rem;
}
.item {
display: flex;
}
.link {
width: 100%;
padding: 0.75rem 1rem;
border-radius: 0.75rem;
color: inherit;
text-decoration: none;
background-color: var(--color-gray-100);
}
.link:focus-visible {
outline: 2px solid var(--color-blue);
outline-offset: -1px;
}
/* ===== Keyframes ===== */
@keyframes backdropShow {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes backdropHide {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
@keyframes slideRightShow {
from {
transform: translateX(100%);
}
to {
transform: translateX(0);
}
}
@keyframes slideRightHide {
from {
transform: translateX(0);
}
to {
transform: translateX(100%);
}
}
@keyframes slideBottomShow {
from {
transform: translateY(100%);
}
to {
transform: translateY(0);
}
}
@keyframes slideBottomCenteredShow {
from {
transform: translateX(-50%) translateY(100%);
}
to {
transform: translateX(-50%) translateY(0);
}
}
@keyframes slideBottomCenteredHide {
from {
transform: translateX(-50%) translateY(0);
}
to {
transform: translateX(-50%) translateY(100%);
}
}
@keyframes slideBottomHide {
from {
transform: translateY(0);
}
to {
transform: translateY(100%);
}
}:root {
--color-blue: oklch(45% 50% 264deg);
--color-red: oklch(50% 55% 31deg);
--color-gray-50: oklch(98% 0.25% 264deg);
--color-gray-100: oklch(12% 9.5% 264deg / 5%);
--color-gray-200: oklch(12% 9% 264deg / 7%);
--color-gray-300: oklch(12% 8.5% 264deg / 17%);
--color-gray-400: oklch(12% 8% 264deg / 38%);
--color-gray-500: oklch(12% 7.5% 264deg / 50%);
--color-gray-600: oklch(12% 7% 264deg / 67%);
--color-gray-700: oklch(12% 6% 264deg / 77%);
--color-gray-800: oklch(12% 5% 264deg / 85%);
--color-gray-900: oklch(12% 5% 264deg / 90%);
--color-gray-950: oklch(12% 5% 264deg / 95%);
}
@media (prefers-color-scheme: dark) {
:root {
--color-blue: oklch(69% 50% 264deg);
--color-red: oklch(80% 55% 31deg);
--color-gray-50: oklch(17% 0.25% 264deg);
--color-gray-100: oklch(28% 0.75% 264deg / 65%);
--color-gray-200: oklch(29% 0.75% 264deg / 80%);
--color-gray-300: oklch(35% 0.75% 264deg / 80%);
--color-gray-400: oklch(47% 0.875% 264deg / 80%);
--color-gray-500: oklch(64% 1% 264deg / 80%);
--color-gray-600: oklch(82% 1% 264deg / 80%);
--color-gray-700: oklch(92% 1.125% 264deg / 80%);
--color-gray-800: oklch(93% 0.875% 264deg / 85%);
--color-gray-900: oklch(95% 0.5% 264deg / 90%);
--color-gray-950: oklch(94% 0.375% 264deg / 95%);
}
}