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%);
  }
}