Slide Menus: 16 Examples + How to Build One That Works

Nielsen Norman Group's 2016 study of 179 participants found that hiding the main navigation behind an icon cut content discoverability by more than 20%. So a slide menu (an off-canvas menu or navigation drawer) works best for secondary links on narrow screens.

Below are 16 slide menu examples you can try and copy, followed by a guide to building, animating and testing them.

By Bogdan Sandu · Updated October 28, 2026

Edge drawers

One panel per edge of the screen: left, right, top and bottom, plus a page that carries two panels at once. Every panel slides with transform, stays inert while closed, and hands focus back to the button that opened it.

Left drawer with a backdrop

  • transform
  • inert
  • focus loop

Open the panel, then try Tab, Escape and a click on the dimmed page.

The classic overlay drawer. The closed panel sits at translateX(-100%) with the inert attribute, so its links are out of the tab order, and opening it moves focus to the first link, loops Tab inside and returns focus to the toggle on close. The demo uses position: absolute to stay inside the phone frame; a real site uses position: fixed and sets inert on the page wrapper.

View code
.sm-left a {
  color: inherit;
  text-decoration: none;
}
.sm-left__bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  border-bottom: 4px double #13213c;
  background: #fff;
  color: #13213c;
}
.sm-left__toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: #13213c;
  color: #fff;
  cursor: pointer;
}
.sm-left__toggle span,
.sm-left__toggle span::before,
.sm-left__toggle span::after {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
}
.sm-left__toggle span {
  position: relative;
}
.sm-left__toggle span::before,
.sm-left__toggle span::after {
  content: "";
  position: absolute;
  left: 0;
}
.sm-left__toggle span::before {
  top: -6px;
}
.sm-left__toggle span::after {
  top: 6px;
  width: 12px;
}
.sm-left__logo {
  font-family: "Fraunces", Georgia, serif;
  font-size: 25px;
  font-weight: 800;
  letter-spacing: -.02em;
}
.sm-left .sm-left__sub {
  margin-left: auto;
  padding: 8px 14px;
  border-radius: 999px;
  background: #b42a24;
  color: #fff;
  font-size: 13px;
  font-weight: 700;
}
.sm-left__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(10, 16, 30, .55);
  opacity: 0;
  visibility: hidden;
}
.sm-left__panel {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(300px, 86%);
  padding: 14px 22px 22px;
  background: #fff;
  color: #13213c;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-left.is-open .sm-left__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-left.is-open .sm-left__panel {
  transform: none;
  visibility: visible;
  box-shadow: 18px 0 40px rgba(10, 16, 30, .25);
}
.sm-left__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-bottom: 10px;
  border-bottom: 1px solid #dfe3ea;
}
.sm-left__title {
  margin: 0;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: #4a5670;
}
.sm-left__close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-right: -10px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
.sm-left__close svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}
.sm-left__list {
  margin: 6px 0 0;
  padding: 0;
  list-style: none;
}
.sm-left__list a {
  display: block;
  padding: 9px 0;
  border-bottom: 1px solid #eef0f4;
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 600;
}
.sm-left__list a:hover {
  color: #b42a24;
}
.sm-left .sm-left__paper {
  display: grid;
  gap: 2px;
  margin-top: auto;
  padding: 14px 16px;
  border-radius: 12px;
  background: #13213c;
  color: #fff;
  font-size: 14px;
}
.sm-left__paper span {
  color: #c9d2e3;
  font-size: 13px;
}
.sm-left button:focus-visible,
.sm-left a:focus-visible {
  outline: 3px solid #b42a24;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-left__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s, box-shadow .35s;
  }
  .sm-left.is-open .sm-left__panel {
    transition-property: transform, box-shadow;
  }
  .sm-left__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Right-edge account panel with a reduced-motion fade

  • translateX(100%)
  • prefers-reduced-motion
  • role="dialog"
Ledgerly

Open the panel, then try Tab, Escape and a click on the dimmed page.

Account and settings links usually live on the right, next to the avatar. The motion is split into two branches: no-preference gets the slide, and reduce swaps it for a short opacity fade, because WCAG does not count opacity changes as motion animation. Swap position: absolute for position: fixed on a real site.

View code
.sm-acct a {
  color: inherit;
  text-decoration: none;
}
.sm-acct__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  background: #0d3b30;
  color: #eafaf4;
}
.sm-acct__brand {
  font-size: 19px;
  font-weight: 800;
  letter-spacing: -.01em;
}
.sm-acct__avatar {
  width: 44px;
  height: 44px;
  padding: 0;
  border: 2px solid #8fe0c4;
  border-radius: 50%;
  background: #e3f6ef;
  color: #0d3b30;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}
.sm-acct__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(6, 28, 22, .5);
  opacity: 0;
  visibility: hidden;
}
.sm-acct__panel {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(290px, 84%);
  padding: 10px 20px 20px;
  background: #f4f8f6;
  color: #12261f;
  transform: translateX(100%);
  visibility: hidden;
}
.sm-acct.is-open .sm-acct__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-acct.is-open .sm-acct__panel {
  transform: none;
  visibility: visible;
  box-shadow: -18px 0 40px rgba(6, 28, 22, .22);
}
.sm-acct__close {
  align-self: flex-end;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-right: -8px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
.sm-acct__close svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}
.sm-acct__who {
  display: grid;
  justify-items: start;
  gap: 2px;
  padding-bottom: 16px;
  border-bottom: 1px solid #d5e2dc;
}
.sm-acct__big {
  display: grid;
  place-items: center;
  width: 56px;
  height: 56px;
  margin-bottom: 8px;
  border-radius: 50%;
  background: #0d3b30;
  color: #8fe0c4;
  font-size: 19px;
  font-weight: 800;
}
.sm-acct__name {
  margin: 0;
  font-size: 18px;
  font-weight: 800;
}
.sm-acct__mail {
  margin: 0;
  color: #4b5f57;
  font-size: 13.5px;
}
.sm-acct__plan {
  margin-top: 8px;
  padding: 4px 10px;
  border-radius: 999px;
  background: #d3f1e5;
  color: #0b5a45;
  font-size: 12px;
  font-weight: 700;
}
.sm-acct__list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}
.sm-acct__list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 44px;
  font-size: 15px;
  font-weight: 600;
}
.sm-acct__list a:hover {
  color: #0b6b52;
}
.sm-acct__list span {
  padding: 2px 8px;
  border-radius: 6px;
  background: #fff1d6;
  color: #7a4a00;
  font-size: 12px;
  font-weight: 700;
}
.sm-acct__out {
  margin-top: auto;
  min-height: 44px;
  border: 1.5px solid #12261f;
  border-radius: 10px;
  background: #fff;
  color: #12261f;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-acct button:focus-visible,
.sm-acct a:focus-visible {
  outline: 3px solid #0b8a6a;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-acct__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s, box-shadow .35s;
  }
  .sm-acct.is-open .sm-acct__panel {
    transition-property: transform, box-shadow;
  }
  .sm-acct__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}
@media (prefers-reduced-motion: reduce) {
  .sm-acct__panel {
    transform: none;
    opacity: 0;
    transition: opacity .2s linear, visibility .2s;
  }
  .sm-acct.is-open .sm-acct__panel {
    opacity: 1;
    transition-property: opacity;
  }
}

Top drop-down sheet

  • translateY(-100%)
  • disclosure button
  • grid tiles

Open the panel, then try Tab, Escape and a click on the dimmed page.

A top sheet suits a short, wide set of choices that would look lost in a tall side panel. It slides down from under the header, so the toggle stays visible and works as its own close button, and it behaves like a disclosure: aria-expanded on the button, Escape and a click on the dimmed page close it. On a real site the sheet would be position: fixed below a fixed header.

View code
.sm-top a {
  color: inherit;
  text-decoration: none;
}
.sm-top__bar {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  border-bottom: 1px solid #ecdcc8;
  background: #fff8ee;
  color: #2a1d14;
}
.sm-top__brand {
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 700;
  font-style: italic;
}
.sm-top__toggle {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-height: 44px;
  padding: 0 14px 0 18px;
  border: 0;
  border-radius: 999px;
  background: #a63d18;
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-top__toggle svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.4;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sm-top.is-open .sm-top__toggle svg {
  transform: rotate(180deg);
}
.sm-top__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(42, 29, 20, .45);
  opacity: 0;
  visibility: hidden;
}
.sm-top__sheet {
  position: absolute;
  top: 65px;
  left: 0;
  right: 0;
  z-index: 4;
  padding: 18px 16px 20px;
  border-radius: 0 0 22px 22px;
  background: #fff8ee;
  color: #2a1d14;
  transform: translateY(-100%);
  visibility: hidden;
}
.sm-top.is-open .sm-top__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-top.is-open .sm-top__sheet {
  transform: none;
  visibility: visible;
  box-shadow: 0 18px 40px rgba(42, 29, 20, .2);
}
.sm-top__tiles {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-top__tiles a {
  display: grid;
  justify-items: center;
  gap: 8px;
  padding: 12px 4px;
  border: 1px solid #ecdcc8;
  border-radius: 14px;
  background: #fff;
  font-size: 13.5px;
  font-weight: 700;
}
.sm-top__tiles a:hover {
  border-color: #a63d18;
}
.sm-top__tiles i {
  width: 34px;
  height: 34px;
  border-radius: 10px;
  background: var(--sm-top-c);
}
.sm-top__label {
  margin: 18px 0 8px;
  color: #6b5444;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
}
.sm-top__chips {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
}
.sm-top__chips a {
  padding: 7px 12px;
  border-radius: 999px;
  background: #f4e4d0;
  font-size: 13px;
  font-weight: 600;
}
.sm-top button:focus-visible,
.sm-top a:focus-visible {
  outline: 3px solid #a63d18;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-top__sheet {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s, box-shadow .35s;
  }
  .sm-top__toggle svg {
    transition: transform .25s ease;
  }
  .sm-top__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Bottom action sheet with drag to close

  • translateY(100%)
  • pointer events
  • touch-action

Hollow Tide · Ep. 112

The lighthouse keepers

Open the panel, then try Tab, Escape and a click on the dimmed page.

Bottom sheets keep actions inside thumb reach. The handle is a real button, so a tap or Enter closes the sheet, and dragging it down more than 80px closes it too, while a shorter drag springs back. touch-action: none on the handle stops the browser from scrolling the page during the drag. Use position: fixed on a real site.

View code
.sm-sheet__row {
  display: flex;
  align-items: center;
  gap: 12px;
  margin: 14px 14px 0;
  padding: 10px;
  border-radius: 16px;
  background: #1c1826;
  color: #f3eef9;
}
.sm-sheet__art {
  flex: none;
  width: 48px;
  height: 48px;
  border-radius: 10px;
  background: #f0b45c;
  box-shadow: inset -14px -14px 0 #3a2f63;
}
.sm-sheet__meta {
  flex: 1;
  min-width: 0;
}
.sm-sheet__show,
.sm-sheet__ep {
  margin: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.sm-sheet__show {
  color: #c4b8dc;
  font-size: 12.5px;
}
.sm-sheet__ep {
  font-size: 15px;
  font-weight: 700;
}
.sm-sheet__more {
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #2d2740;
  color: #f3eef9;
  cursor: pointer;
}
.sm-sheet__more svg {
  fill: currentColor;
}
.sm-sheet__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(12, 10, 18, .55);
  opacity: 0;
  visibility: hidden;
}
.sm-sheet__panel {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  z-index: 4;
  padding: 4px 18px 18px;
  border-radius: 24px 24px 0 0;
  background: #1c1826;
  color: #f3eef9;
  transform: translateY(100%);
  visibility: hidden;
}
.sm-sheet.is-open .sm-sheet__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-sheet.is-open .sm-sheet__panel {
  transform: none;
  visibility: visible;
}
.sm-sheet__handle {
  display: grid;
  place-items: center;
  width: 100%;
  height: 32px;
  padding: 0;
  border: 0;
  background: none;
  cursor: grab;
  touch-action: none;
}
.sm-sheet__handle span {
  width: 44px;
  height: 5px;
  border-radius: 5px;
  background: #6d6385;
}
.sm-sheet__title {
  margin: 0 0 6px;
  font-size: 13px;
  font-weight: 700;
  color: #c4b8dc;
}
.sm-sheet__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-sheet__list button,
.sm-sheet__list a {
  display: flex;
  align-items: center;
  width: 100%;
  min-height: 46px;
  padding: 0;
  border: 0;
  border-bottom: 1px solid #2f2943;
  background: none;
  color: #f3eef9;
  font: inherit;
  font-size: 15.5px;
  font-weight: 600;
  text-align: left;
  text-decoration: none;
  cursor: pointer;
}
.sm-sheet__list li:last-child a {
  border-bottom: 0;
  color: #f0b45c;
}
.sm-sheet button:focus-visible,
.sm-sheet a:focus-visible {
  outline: 3px solid #f0b45c;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-sheet__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-sheet.is-open .sm-sheet__panel {
    transition-property: transform;
  }
  .sm-sheet__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
  .sm-sheet.is-dragging .sm-sheet__panel {
    transition: none;
  }
}

Two panels, one on each side

  • one open at a time
  • checkbox list
  • inline SVG map
Glen Arra trail

Map layers

Open the panel, then try Tab, Escape and a click on the dimmed page.

A map app with navigation on the left and map layers on the right. Each panel has its own toggle and its own label, and opening one closes the other, the same rule Bootstrap applies to offcanvas. The layer switches change the map underneath right away, which is why this panel has no backdrop and no aria-modal. Use position: fixed on a real site.

View code
.sm-duo {
  position: relative;
  height: 100%;
  overflow: hidden;
  color: #1f3a2e;
}
.sm-duo a {
  color: inherit;
  text-decoration: none;
}
.sm-duo__map {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
}
.sm-duo__map .is-off {
  opacity: 0;
}
.sm-duo__bar {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  margin: 12px;
  padding: 6px;
  border-radius: 16px;
  background: #fff;
  box-shadow: 0 6px 20px rgba(31, 58, 46, .18);
}
.sm-duo__place {
  overflow: hidden;
  font-size: 14px;
  font-weight: 700;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.sm-duo__btn {
  flex: none;
  min-height: 44px;
  padding: 0 14px;
  border: 0;
  border-radius: 12px;
  background: #e5ece3;
  color: #1f3a2e;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-duo__btn[aria-expanded="true"] {
  background: #1f3a2e;
  color: #fff;
}
.sm-duo__panel {
  position: absolute;
  top: 80px;
  bottom: 12px;
  z-index: 4;
  width: min(220px, 64%);
  padding: 16px;
  border-radius: 18px;
  background: #fff;
  box-shadow: 0 10px 30px rgba(31, 58, 46, .2);
  visibility: hidden;
}
.sm-duo__panel--start {
  left: 12px;
  transform: translateX(calc(-100% - 24px));
}
.sm-duo__panel--end {
  right: 12px;
  transform: translateX(calc(100% + 24px));
}
.sm-duo__panel.is-open {
  transform: none;
  visibility: visible;
}
.sm-duo__title {
  margin: 0 0 10px;
  font-size: 17px;
  font-weight: 800;
}
.sm-duo__links {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-duo__links a {
  display: flex;
  align-items: center;
  min-height: 44px;
  border-bottom: 1px solid #e5ece3;
  font-size: 15px;
  font-weight: 600;
}
.sm-duo__check {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  font-size: 14.5px;
  font-weight: 600;
  cursor: pointer;
}
.sm-duo__check input {
  width: 20px;
  height: 20px;
  margin: 0;
  accent-color: #1f3a2e;
}
.sm-duo button:focus-visible,
.sm-duo a:focus-visible,
.sm-duo input:focus-visible {
  outline: 3px solid #c2410c;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-duo__panel {
    transition: transform .3s cubic-bezier(.2, .8, .2, 1), visibility .3s;
  }
}

Push, full-screen and drill-down menus

Menus that do more than cover the page: one pushes it aside, one takes the whole screen, one drills into submenus, and one filters its own links. The native dialog and checkbox versions show what the platform gives you for free and what it does not.

Push menu that moves the page

  • translateX on the page
  • no aria-modal
  • inert

40 minutes · serves 4

Charred tomato and white bean stew

  • 2 tins cannellini beans
  • 500 g cherry tomatoes
  • 4 cloves garlic, sliced
  • Thyme, olive oil, sourdough

Here the panel waits behind the page and the page itself slides right by the panel width. Because the page stays visible and clickable, the panel is plain navigation with aria-expanded on the toggle and no aria-modal, as the guide below explains. On a real site, a transformed page wrapper breaks position: fixed headers inside it, so keep fixed elements outside the wrapper.

View code
.sm-push {
  position: relative;
  height: 100%;
  overflow: hidden;
  background: #2f3820;
}
.sm-push a {
  color: inherit;
  text-decoration: none;
}
.sm-push__nav {
  position: absolute;
  inset: 0 auto 0 0;
  display: flex;
  flex-direction: column;
  width: min(240px, 72%);
  padding: 22px 20px;
  color: #f4efe1;
}
.sm-push__brand {
  margin: 0 0 14px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 700;
}
.sm-push__nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-push__nav li a {
  display: flex;
  align-items: center;
  min-height: 44px;
  border-bottom: 1px solid #4a5534;
  font-size: 15.5px;
  font-weight: 600;
}
.sm-push__nav li a:hover {
  color: #f2b98f;
}
.sm-push .sm-push__cta {
  margin-top: auto;
  padding: 12px 14px;
  border-radius: 10px;
  background: #f4efe1;
  color: #2f3820;
  font-weight: 700;
  text-align: center;
}
.sm-push__page {
  position: relative;
  z-index: 2;
  height: 100%;
  background: #f8f4ea;
  color: #2a2a1f;
}
.sm-push.is-open .sm-push__page {
  transform: translateX(min(240px, 72%));
  box-shadow: -14px 0 30px rgba(0, 0, 0, .3);
}
.sm-push__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #e6dcc6;
}
.sm-push__toggle {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 16px 0 14px;
  border: 0;
  border-radius: 10px;
  background: #3f4a2a;
  color: #f8f4ea;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-push__toggle span,
.sm-push__toggle span::before,
.sm-push__toggle span::after {
  display: block;
  width: 16px;
  height: 2px;
  background: currentColor;
}
.sm-push__toggle span {
  position: relative;
}
.sm-push__toggle span::before,
.sm-push__toggle span::after {
  content: "";
  position: absolute;
  left: 0;
}
.sm-push__toggle span::before {
  top: -5px;
}
.sm-push__toggle span::after {
  top: 5px;
}
.sm-push__logo {
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 700;
  color: #a8391f;
}
.sm-push__body {
  padding: 18px 16px;
}
.sm-push__hero {
  position: relative;
  height: 170px;
  margin-bottom: 16px;
  overflow: hidden;
  border-radius: 18px;
  background: #e9d7b4;
}
.sm-push__hero i {
  position: absolute;
  border-radius: 50%;
}
.sm-push__hero i:nth-child(1) {
  inset: 22px auto auto 50%;
  width: 150px;
  height: 150px;
  margin-left: -75px;
  background: #fbf6ec;
  box-shadow: 0 0 0 10px #d9c49c;
}
.sm-push__hero i:nth-child(2) {
  top: 62px;
  left: calc(50% - 40px);
  width: 34px;
  height: 34px;
  background: #c4472c;
  box-shadow: 46px 14px 0 #d4582f, 18px 42px 0 #b93d24;
}
.sm-push__hero i:nth-child(3) {
  top: 80px;
  left: calc(50% - 6px);
  width: 14px;
  height: 14px;
  background: #6e7d45;
  box-shadow: -30px 30px 0 #6e7d45, 34px 34px 0 #6e7d45;
}
.sm-push__kicker {
  margin: 0;
  color: #6b5d3f;
  font-size: 13px;
  font-weight: 600;
}
.sm-push__dish {
  margin: 4px 0 12px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 23px;
  font-weight: 700;
  line-height: 1.2;
}
.sm-push__ing {
  margin: 0;
  padding-left: 18px;
  font-size: 14.5px;
}
.sm-push button:focus-visible,
.sm-push a:focus-visible {
  outline: 3px solid #f2b98f;
  outline-offset: 2px;
}
.sm-push__page button:focus-visible {
  outline-color: #a8391f;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-push__page {
    transition: transform .4s cubic-bezier(.2, .8, .2, 1), box-shadow .4s;
  }
}

Full-screen menu with staggered links

  • @keyframes
  • animation-delay
  • custom properties

The panel drops over the whole screen, then each link rises in a few milliseconds after the one before it. The delay comes from a --sm-fs-i index on every item, and both the slide and the keyframes sit inside prefers-reduced-motion: no-preference, so reduced-motion users get the menu instantly. A real site would use position: fixed and inset: 0.

View code
.sm-fs a {
  color: inherit;
  text-decoration: none;
}
.sm-fs__bar,
.sm-fs__top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 18px;
}
.sm-fs__bar {
  border-bottom: 1px solid #e4e1d8;
  background: #fff;
  color: #101312;
}
.sm-fs__logo {
  font-family: "Fraunces", Georgia, serif;
  font-size: 19px;
  font-weight: 600;
  letter-spacing: .01em;
}
.sm-fs__open,
.sm-fs__close {
  min-height: 44px;
  padding: 0 18px;
  border: 1.5px solid currentColor;
  border-radius: 999px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-fs__panel {
  position: absolute;
  inset: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  background: #121614;
  color: #efe9dc;
  transform: translateY(-100%);
  visibility: hidden;
}
.sm-fs.is-open .sm-fs__panel {
  transform: none;
  visibility: visible;
}
.sm-fs__links {
  margin: 0;
  padding: 18px 22px;
  list-style: none;
}
.sm-fs__links a {
  display: inline-block;
  padding: 6px 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: clamp(36px, 11vw, 46px);
  font-weight: 500;
  line-height: 1.15;
}
.sm-fs__links a:hover {
  color: #e2b04d;
  font-style: italic;
}
.sm-fs__foot {
  margin-top: auto;
  padding: 18px 22px 24px;
  border-top: 1px solid #2c332f;
  font-size: 14px;
}
.sm-fs__foot p {
  margin: 0 0 4px;
  color: #b9b3a6;
}
.sm-fs__foot a {
  color: #e2b04d;
  font-weight: 600;
}
.sm-fs button:focus-visible,
.sm-fs a:focus-visible {
  outline: 3px solid #e2b04d;
  outline-offset: 3px;
}
.sm-fs__bar button:focus-visible {
  outline-color: #8a5a00;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-fs__panel {
    transition: transform .5s cubic-bezier(.7, 0, .2, 1), visibility .5s;
  }
  .sm-fs.is-open .sm-fs__panel {
    transition-property: transform;
  }
  .sm-fs.is-open .sm-fs__links li {
    animation: sm-fs-rise .55s cubic-bezier(.2, .8, .2, 1) both;
    animation-delay: calc(var(--sm-fs-i) * 70ms + 260ms);
  }
}
@keyframes sm-fs-rise {
  from {
    opacity: 0;
    transform: translateY(28px);
  }
}

Multi-level drill-down with a back button

  • sliding levels
  • back button
  • focus restore

Submenus replace the current list instead of expanding under it, so there is never a scroll area inside a scroll area. Each level slides in from the right, the hidden levels are inert, and the back button sends focus to the item that opened the level. Keep it to two or three levels, since every level adds a tap. Use position: fixed for the panel on a real site.

View code
.sm-dd a {
  color: inherit;
  text-decoration: none;
}
.sm-dd__bar {
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 10px 16px;
  background: #22303c;
  color: #f1f4f6;
}
.sm-dd__toggle {
  min-height: 44px;
  padding: 0 18px;
  border: 0;
  border-radius: 8px;
  background: #e8b931;
  color: #1a232b;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  cursor: pointer;
}
.sm-dd__logo {
  font-size: 15px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
}
.sm-dd__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(16, 22, 28, .55);
  opacity: 0;
  visibility: hidden;
}
.sm-dd__panel {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(300px, 86%);
  overflow: hidden;
  background: #fff;
  color: #1a232b;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-dd.is-open .sm-dd__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-dd.is-open .sm-dd__panel {
  transform: none;
  visibility: visible;
}
.sm-dd__close {
  align-self: flex-end;
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin: 8px 8px 0;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
.sm-dd__close svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}
.sm-dd__levels {
  position: relative;
  flex: 1;
}
.sm-dd__level {
  position: absolute;
  inset: 0;
  padding: 0 20px;
  background: #fff;
  transform: translateX(100%);
  visibility: hidden;
}
.sm-dd__level.is-current {
  transform: none;
  visibility: visible;
}
.sm-dd__level.is-past {
  transform: translateX(-30%);
}
.sm-dd__heading {
  margin: 0 0 6px;
  font-size: 26px;
  font-weight: 800;
  letter-spacing: -.01em;
}
.sm-dd__level ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-dd__level li a,
.sm-dd__next {
  display: flex;
  align-items: center;
  justify-content: space-between;
  width: 100%;
  min-height: 50px;
  padding: 0;
  border: 0;
  border-bottom: 1px solid #e6eaee;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 16px;
  font-weight: 600;
  text-align: left;
  cursor: pointer;
}
.sm-dd__next::after {
  content: "";
  width: 8px;
  height: 8px;
  border-top: 2px solid currentColor;
  border-right: 2px solid currentColor;
  transform: rotate(45deg);
}
.sm-dd__back {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 40px;
  margin-bottom: 4px;
  padding: 0;
  border: 0;
  background: none;
  color: #3b5f7d;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-dd__back::before {
  content: "";
  width: 7px;
  height: 7px;
  border-bottom: 2px solid currentColor;
  border-left: 2px solid currentColor;
  transform: rotate(45deg);
}
.sm-dd button:focus-visible,
.sm-dd a:focus-visible {
  outline: 3px solid #3b5f7d;
  outline-offset: 2px;
}
.sm-dd__bar button:focus-visible {
  outline-color: #e8b931;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-dd__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-dd.is-open .sm-dd__panel {
    transition-property: transform;
  }
  .sm-dd__level {
    transition: transform .3s cubic-bezier(.2, .8, .2, 1), visibility .3s;
  }
  .sm-dd__level.is-current {
    transition-property: transform;
  }
  .sm-dd__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Docs panel with search and grouped sections

  • live filter
  • role="status"
  • grouped lists
v3.2

Documentation menus get long, so the panel opens with a search field that filters the links as you type and hides any group left empty. A role="status" line announces how many pages match, and the field gets focus on open. On a real site the panel is position: fixed and the filter would also match page keywords, not only titles.

View code
.sm-docs a {
  color: inherit;
  text-decoration: none;
}
.sm-docs__bar {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 10px 14px;
  border-bottom: 1px solid #e3e6ef;
  background: #fff;
  color: #1d2433;
}
.sm-docs__toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 1px solid #d5dae6;
  border-radius: 10px;
  background: #f5f6fa;
  color: inherit;
  cursor: pointer;
}
.sm-docs__toggle svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linecap: round;
}
.sm-docs__logo {
  font-size: 17px;
  font-weight: 800;
}
.sm-docs__logo span {
  color: #3d4fd1;
}
.sm-docs__ver {
  margin-left: auto;
  padding: 3px 8px;
  border-radius: 6px;
  background: #eceefb;
  color: #3240a8;
  font-family: ui-monospace, Consolas, monospace;
  font-size: 12px;
}
.sm-docs__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(20, 25, 40, .5);
  opacity: 0;
  visibility: hidden;
}
.sm-docs__panel {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(310px, 88%);
  background: #f7f8fb;
  color: #1d2433;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-docs.is-open .sm-docs__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-docs.is-open .sm-docs__panel {
  transform: none;
  visibility: visible;
}
.sm-docs__search {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  gap: 6px 8px;
  padding: 14px 14px 8px;
}
.sm-docs__label {
  grid-column: 1 / -1;
  font-size: 12.5px;
  font-weight: 700;
  color: #4a5268;
}
.sm-docs__input {
  min-width: 0;
  min-height: 44px;
  padding: 0 12px;
  border: 1.5px solid #c9cfdd;
  border-radius: 10px;
  background: #fff;
  color: #1d2433;
  font: inherit;
  font-size: 15px;
}
.sm-docs__input::placeholder {
  color: #6b7389;
}
.sm-docs__close {
  min-height: 44px;
  padding: 0 14px;
  border: 0;
  border-radius: 10px;
  background: #3d4fd1;
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-docs__count {
  margin: 0;
  padding: 0 14px 6px;
  color: #4a5268;
  font-size: 12.5px;
}
.sm-docs__nav {
  flex: 1;
  overflow-y: auto;
  padding: 0 14px 16px;
}
.sm-docs__gtitle {
  margin: 12px 0 2px;
  font-size: 11.5px;
  font-weight: 800;
  letter-spacing: .1em;
  text-transform: uppercase;
  color: #5b6378;
}
.sm-docs__group ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-docs__group a {
  display: flex;
  align-items: center;
  min-height: 36px;
  padding: 0 10px;
  border-radius: 8px;
  font-size: 14.5px;
}
.sm-docs__group a:hover {
  background: #eceefb;
}
.sm-docs__group a[aria-current="page"] {
  background: #3d4fd1;
  color: #fff;
  font-weight: 700;
}
.sm-docs__empty {
  margin: 16px 0 0;
  font-size: 14px;
  color: #4a5268;
}
.sm-docs__group[hidden],
.sm-docs li[hidden],
.sm-docs__empty[hidden] {
  display: none;
}
.sm-docs button:focus-visible,
.sm-docs a:focus-visible,
.sm-docs input:focus-visible {
  outline: 3px solid #3d4fd1;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-docs__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-docs.is-open .sm-docs__panel {
    transition-property: transform;
  }
  .sm-docs__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Native dialog drawer

  • showModal()
  • ::backdrop
  • @starting-style
Field notes / Spring survey

Heron count, north marsh

Seven adults on the reed edge at 06:40, two on the old jetty. Water level lower than last week. Check the east hide on Thursday.

showModal() puts the drawer in the top layer, makes the rest of the page inert, closes on Escape and gives you a ::backdrop to style, with no focus trap code. The slide uses @starting-style plus transition-behavior: allow-discrete on display and overlay. Firefox and Safari do not support the overlay property yet, and Firefox does not transition display, so there the drawer still slides in but the closing slide can be cut short.

View code
.sm-dlg {
  width: min(560px, 100%);
}
.sm-dlg a {
  color: inherit;
  text-decoration: none;
}
.sm-dlg__app {
  padding: 18px 20px 22px;
  border: 1px solid #e7e1cf;
  border-radius: 18px;
  background: #fffdf6;
  color: #26231b;
  box-shadow: 0 14px 40px rgba(38, 35, 27, .1);
}
.sm-dlg__bar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 10px 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid #ece6d4;
}
.sm-dlg__open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 10px;
  background: #2f4a3a;
  color: #fffdf6;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-dlg__open svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2;
  stroke-linejoin: round;
}
.sm-dlg__crumb {
  color: #6a6350;
  font-size: 13.5px;
}
.sm-dlg__note-title {
  margin: 16px 0 6px;
  font-family: "Fraunces", Georgia, serif;
  font-size: 22px;
  font-weight: 600;
}
.sm-dlg__note-text {
  margin: 0;
  font-size: 15px;
  line-height: 1.6;
  color: #4a4535;
}
.sm-dlg__drawer {
  inset: 0 auto 0 0;
  width: min(330px, 88vw);
  max-width: none;
  height: auto;
  max-height: none;
  margin: 0;
  padding: 0;
  border: 0;
  background: #fffdf6;
  color: #26231b;
  box-shadow: 20px 0 50px rgba(20, 18, 12, .3);
}
.sm-dlg__drawer::backdrop {
  background: rgba(20, 18, 12, .5);
}
.sm-dlg__inner {
  display: flex;
  flex-direction: column;
  min-height: 100%;
  padding: 18px 22px 24px;
  box-sizing: border-box;
}
.sm-dlg__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: 10px;
}
.sm-dlg__title {
  margin: 0;
  font-family: "Fraunces", Georgia, serif;
  font-size: 24px;
  font-weight: 600;
}
.sm-dlg__close {
  min-height: 44px;
  padding: 0 14px;
  border: 1.5px solid #cfc7b0;
  border-radius: 10px;
  background: none;
  color: inherit;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-dlg__list {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-dlg__list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 46px;
  padding: 0 12px;
  border-radius: 10px;
  font-size: 15.5px;
  font-weight: 600;
}
.sm-dlg__list a:hover {
  background: #f1ecdc;
}
.sm-dlg__list a[aria-current="page"] {
  background: #2f4a3a;
  color: #fffdf6;
}
.sm-dlg__list span {
  font-size: 13px;
  font-weight: 500;
  opacity: .8;
}
.sm-dlg__new {
  margin-top: auto;
  min-height: 44px;
  border: 1.5px dashed #8a8169;
  border-radius: 10px;
  background: none;
  color: #26231b;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-dlg button:focus-visible,
.sm-dlg a:focus-visible {
  outline: 3px solid #b06a1f;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-dlg__drawer {
    transform: translateX(-100%);
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), overlay .35s allow-discrete, display .35s allow-discrete;
  }
  .sm-dlg__drawer[open] {
    transform: none;
  }
  .sm-dlg__drawer::backdrop {
    opacity: 0;
    transition: opacity .35s ease, overlay .35s allow-discrete, display .35s allow-discrete;
  }
  .sm-dlg__drawer[open]::backdrop {
    opacity: 1;
  }
  @starting-style {
    .sm-dlg__drawer[open] {
      transform: translateX(-100%);
    }
    .sm-dlg__drawer[open]::backdrop {
      opacity: 0;
    }
  }
}

CSS-only checkbox slide menu

  • :checked
  • label for
  • no JavaScript

A visually hidden checkbox holds the state and the labels flip it, so the panel opens with zero JavaScript. Be honest about the limits: screen readers hear a checkbox instead of an expanded button, focus stays behind the panel, Escape does nothing, and keyboard users toggle it with Space on the hidden box. visibility: hidden keeps the closed links out of the tab order. Fine for a prototype; use the scripted version for production.

View code
.sm-chk a {
  color: inherit;
  text-decoration: none;
}
.sm-chk__state {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}
.sm-chk__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 10px 16px;
  background: #3b2418;
  color: #f7ebdd;
}
.sm-chk__logo {
  font-family: "Fraunces", Georgia, serif;
  font-size: 20px;
  font-weight: 700;
}
.sm-chk__label {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  min-height: 44px;
  padding: 0 16px;
  border: 1.5px solid #c99a6b;
  border-radius: 999px;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-chk__label span,
.sm-chk__label span::before {
  display: block;
  width: 16px;
  height: 2px;
  background: currentColor;
}
.sm-chk__label span {
  position: relative;
  margin-top: -6px;
}
.sm-chk__label span::before {
  content: "";
  position: absolute;
  top: 6px;
}
.sm-chk__state:focus-visible ~ .sm-chk__bar .sm-chk__label {
  outline: 3px solid #f0b67a;
  outline-offset: 2px;
}
.sm-chk__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(30, 18, 12, .55);
  opacity: 0;
  visibility: hidden;
}
.sm-chk__panel {
  position: absolute;
  inset: 0 0 0 auto;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(280px, 82%);
  padding: 12px 22px 22px;
  background: #f7ebdd;
  color: #3b2418;
  transform: translateX(100%);
  visibility: hidden;
}
.sm-chk__state:checked ~ .sm-chk__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-chk__state:checked ~ .sm-chk__panel {
  transform: none;
  visibility: visible;
}
.sm-chk__close {
  align-self: flex-end;
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 4px;
  font-size: 13px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
}
.sm-chk__panel ul {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}
.sm-chk__panel li a {
  display: block;
  padding: 10px 0;
  border-bottom: 1px solid #e3cfb8;
  font-family: "Fraunces", Georgia, serif;
  font-size: 21px;
  font-weight: 600;
}
.sm-chk__panel li a:hover {
  color: #9a4a1c;
}
.sm-chk__promo {
  margin: auto 0 0;
  padding: 12px 14px;
  border-radius: 12px;
  background: #3b2418;
  color: #f7ebdd;
  font-size: 13.5px;
  font-weight: 600;
}
.sm-chk a:focus-visible {
  outline: 3px solid #9a4a1c;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-chk__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-chk__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Responsive, desktop and RTL panels

Slide menus at wider sizes, where they turn into sidebars, icon rails, filter panels and slide-overs, plus a right-to-left version built on logical properties. The desktop frames shrink to phone width on small screens without breaking.

Drawer that becomes a sidebar

  • container query
  • ResizeObserver
  • inert reset
Bramble

Pipeline

Lead 8

Proposal 5

Won 3

Switch the frame width on a wide screen to cross the 640px breakpoint with the drawer open.

Below 640px of container width the navigation is a drawer behind a Menu button; above it, a container query turns the same nav into a visible sidebar and hides the toggle. A ResizeObserver closes the drawer and clears inert the moment the frame crosses the breakpoint, so nothing stays stuck in the open state. A page-level version would use a media query and position: fixed.

View code
.sm-resp {
  padding: 22px 16px 28px;
  background: #eef2ea;
}
.sm-resp a {
  color: inherit;
  text-decoration: none;
}
.sm-resp__switch {
  display: flex;
  justify-content: center;
  gap: 6px;
  margin-bottom: 16px;
}
.sm-resp__w {
  min-height: 40px;
  padding: 0 14px;
  border: 1.5px solid #b9c7b2;
  border-radius: 999px;
  background: #fff;
  color: #1f3d2b;
  font: inherit;
  font-size: 13.5px;
  font-weight: 700;
  cursor: pointer;
}
.sm-resp__w[aria-pressed="true"] {
  border-color: #1f3d2b;
  background: #1f3d2b;
  color: #fff;
}
.sm-resp__frame {
  position: relative;
  width: 100%;
  height: 380px;
  margin: 0 auto;
  overflow: hidden;
  border: 1px solid #cfd9c9;
  border-radius: 18px;
  background: #fff;
  container: sm-resp / inline-size;
}
.sm-resp__frame.is-narrow {
  width: min(360px, 100%);
}
.sm-resp__app {
  height: 100%;
  color: #1c2a21;
}
.sm-resp__bar {
  display: flex;
  align-items: center;
  gap: 12px;
  height: 64px;
  padding: 0 14px;
  border-bottom: 1px solid #e3e9df;
  box-sizing: border-box;
}
.sm-resp__toggle {
  min-height: 44px;
  padding: 0 16px;
  border: 0;
  border-radius: 10px;
  background: #1f3d2b;
  color: #fff;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-resp__brand {
  font-size: 18px;
  font-weight: 800;
  color: #1f3d2b;
}
.sm-resp__user {
  display: grid;
  place-items: center;
  width: 34px;
  height: 34px;
  margin-left: auto;
  border-radius: 50%;
  background: #f1d9b5;
  color: #5a3a10;
  font-size: 12px;
  font-weight: 800;
}
.sm-resp__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(20, 34, 25, .5);
  opacity: 0;
  visibility: hidden;
}
.sm-resp__panel {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  width: min(240px, 80%);
  padding: 16px 12px;
  background: #f6f8f3;
  box-sizing: border-box;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-resp.is-open .sm-resp__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-resp.is-open .sm-resp__panel {
  transform: none;
  visibility: visible;
  box-shadow: 16px 0 36px rgba(20, 34, 25, .2);
}
.sm-resp__panel ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-resp__panel a {
  display: flex;
  align-items: center;
  min-height: 42px;
  padding: 0 12px;
  border-radius: 10px;
  font-size: 15px;
  font-weight: 600;
}
.sm-resp__panel a:hover {
  background: #e3ebdd;
}
.sm-resp__panel a[aria-current="page"] {
  background: #1f3d2b;
  color: #fff;
}
.sm-resp__main {
  padding: 16px;
}
.sm-resp__h {
  margin: 0 0 12px;
  font-size: 20px;
  font-weight: 800;
}
.sm-resp__cols {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(96px, 1fr));
  gap: 10px;
}
.sm-resp__col {
  padding: 10px;
  border-radius: 12px;
  background: #f3f6f0;
}
.sm-resp__col p {
  display: flex;
  justify-content: space-between;
  margin: 0 0 8px;
  font-size: 13px;
  font-weight: 700;
}
.sm-resp__col span {
  color: #46604f;
}
.sm-resp__col i {
  display: block;
  height: 46px;
  margin-top: 8px;
  border-radius: 8px;
  background: #fff;
  box-shadow: inset 4px 0 0 #7fae8d;
}
.sm-resp button:focus-visible,
.sm-resp a:focus-visible {
  outline: 3px solid #b5651d;
  outline-offset: 2px;
}
@container sm-resp (min-width: 640px) {
  .sm-resp__app {
    display: grid;
    grid-template-columns: 220px minmax(0, 1fr);
    grid-template-rows: 64px minmax(0, 1fr);
  }
  .sm-resp__bar {
    grid-column: 1 / -1;
  }
  .sm-resp__toggle,
  .sm-resp__backdrop {
    display: none;
  }
  .sm-resp__panel,
  .sm-resp.is-open .sm-resp__panel {
    position: static;
    width: auto;
    border-right: 1px solid #e3e9df;
    transform: none;
    visibility: visible;
    box-shadow: none;
    transition: none;
  }
}
@media (prefers-reduced-motion: no-preference) {
  .sm-resp__frame {
    transition: width .4s ease;
  }
  .sm-resp__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-resp.is-open .sm-resp__panel {
    transition-property: transform;
  }
  .sm-resp__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Icon rail that expands into a drawer

  • clip-path reveal
  • aria-expanded
  • overlay

Weekly overview

Sessions48,210
Conversion3.4%
Revenue$12.9k

The full 232px drawer is always laid out, and clip-path shows only its first 64px until the toggle expands it. Because the drawer overlays the dashboard instead of resizing it, nothing reflows when it opens, and the icons never jump. The link text stays in the DOM, so the collapsed icons still have accessible names. In a real app the rail is position: fixed against the viewport edge.

View code
.sm-rail {
  position: relative;
  width: min(720px, 100%);
  height: 380px;
  overflow: hidden;
  border-radius: 18px;
  background: #f5f5f8;
  color: #1b1e27;
  box-shadow: 0 14px 40px rgba(27, 30, 39, .12);
}
.sm-rail a {
  color: inherit;
  text-decoration: none;
}
.sm-rail__nav {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  width: 232px;
  padding: 10px;
  box-sizing: border-box;
  background: #1b1e27;
  color: #e8eaf0;
  clip-path: inset(0 calc(100% - 64px) 0 0);
}
.sm-rail.is-open .sm-rail__nav {
  clip-path: inset(0);
}
.sm-rail__toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  margin-bottom: 10px;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: #2a2e3b;
  color: inherit;
  cursor: pointer;
}
.sm-rail svg {
  flex: none;
  width: 22px;
  height: 22px;
  fill: none;
  stroke: currentColor;
  stroke-width: 1.8;
  stroke-linecap: round;
  stroke-linejoin: round;
}
.sm-rail__nav ul {
  margin: 0;
  padding: 0;
  list-style: none;
}
.sm-rail__nav a {
  display: flex;
  align-items: center;
  gap: 16px;
  min-height: 44px;
  margin-bottom: 4px;
  padding: 0 11px;
  border-radius: 10px;
  font-size: 14.5px;
  font-weight: 600;
  white-space: nowrap;
}
.sm-rail__nav a:hover {
  background: #2a2e3b;
}
.sm-rail__nav a[aria-current="page"] {
  background: #ffb547;
  color: #1b1e27;
}
.sm-rail__nav span {
  opacity: 0;
}
.sm-rail.is-open .sm-rail__nav span {
  opacity: 1;
}
.sm-rail__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(27, 30, 39, .4);
  opacity: 0;
  visibility: hidden;
}
.sm-rail.is-open .sm-rail__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-rail__main {
  height: 100%;
  margin-left: 64px;
  padding: 20px;
  box-sizing: border-box;
}
.sm-rail__h {
  margin: 0 0 14px;
  font-size: 19px;
  font-weight: 800;
}
.sm-rail__stats {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(110px, 1fr));
  gap: 10px;
}
.sm-rail__stats div {
  padding: 12px 14px;
  border-radius: 12px;
  background: #fff;
}
.sm-rail__stats span {
  display: block;
  color: #5b6070;
  font-size: 12.5px;
  font-weight: 600;
}
.sm-rail__stats strong {
  font-size: 20px;
}
.sm-rail__chart {
  display: flex;
  align-items: flex-end;
  gap: 8px;
  height: 120px;
  margin-top: 14px;
  padding: 14px;
  border-radius: 12px;
  background: #fff;
}
.sm-rail__chart i {
  flex: 1;
  border-radius: 6px 6px 2px 2px;
  background: #c9cde0;
}
.sm-rail__chart i:nth-child(6) {
  background: #ffb547;
}
.sm-rail button:focus-visible,
.sm-rail a:focus-visible {
  outline: 3px solid #ffb547;
  outline-offset: -3px;
}
.sm-rail__backdrop,
.sm-rail__nav span {
  transition: opacity .25s ease, visibility .25s;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-rail__nav {
    transition: clip-path .35s cubic-bezier(.2, .8, .2, 1);
  }
}

Shop filter panel

  • fieldset
  • live result count
  • form submit

6 prints

  • Deep blue fjord between grey cliffs, seen from a crowded rock ledge

    Fjord Ledge €42

  • Red sandstone canyon walls glowing orange at sunset

    Canyon Dusk €38

  • City skyline at golden hour with a lake stretching to the horizon

    Lakeside Towers €45

  • Pine-covered cliffs dropping to a calm sea in evening light

    Cliff Road €29

  • Old brick houses and moored boats along a quiet canal

    Canal Morning €34

  • Single-track road winding through green highland cliffs under low cloud

    Highland Pass €48

Filter prints

Subject
Size

Filters are a natural slide-in: they are needed now and then, and they would crowd the grid if they stayed visible on small screens. The apply button counts matching prints live as you tick boxes or drag the price, and submitting filters the grid and closes the panel. The form never leaves the page because the submit handler calls preventDefault(). Make it position: fixed on a real shop.

View code
.sm-shop {
  position: relative;
  width: min(760px, 100%);
  height: 470px;
  overflow: hidden;
  border-radius: 18px;
  background: #fff;
  color: #1e1b18;
  box-shadow: 0 14px 40px rgba(30, 27, 24, .1);
}
.sm-shop__bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 14px 16px;
  border-bottom: 1px solid #ece8e2;
}
.sm-shop__open {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-height: 44px;
  padding: 0 16px;
  border: 1.5px solid #1e1b18;
  border-radius: 999px;
  background: #fff;
  color: #1e1b18;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-shop__open svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}
.sm-shop__total {
  margin: 0;
  color: #5d564d;
  font-size: 14px;
}
.sm-shop__grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  gap: 14px;
  height: calc(100% - 73px);
  margin: 0;
  padding: 16px;
  overflow-y: auto;
  box-sizing: border-box;
  list-style: none;
}
.sm-shop__grid li[hidden] {
  display: none;
}
.sm-shop__grid img {
  display: block;
  width: 100%;
  height: auto;
  aspect-ratio: 4 / 3;
  object-fit: cover;
  border-radius: 10px;
}
.sm-shop__grid p {
  display: flex;
  justify-content: space-between;
  gap: 8px;
  margin: 8px 0 0;
  font-size: 14px;
  font-weight: 700;
}
.sm-shop__grid span {
  color: #5d564d;
  font-weight: 500;
}
.sm-shop__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(30, 27, 24, .45);
  opacity: 0;
  visibility: hidden;
}
.sm-shop__panel {
  position: absolute;
  inset: 0 auto 0 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(300px, 88%);
  margin: 0;
  background: #faf8f5;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-shop.is-open .sm-shop__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-shop.is-open .sm-shop__panel {
  transform: none;
  visibility: visible;
  box-shadow: 16px 0 40px rgba(30, 27, 24, .2);
}
.sm-shop__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 8px 10px 8px 18px;
  border-bottom: 1px solid #ece8e2;
}
.sm-shop__title {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
}
.sm-shop__close {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: inherit;
  cursor: pointer;
}
.sm-shop__close svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 2.2;
  stroke-linecap: round;
}
.sm-shop__body {
  flex: 1;
  overflow-y: auto;
  padding: 6px 18px 12px;
}
.sm-shop fieldset {
  margin: 0;
  padding: 10px 0;
  border: 0;
  border-bottom: 1px solid #ece8e2;
}
.sm-shop legend {
  padding: 0;
  margin-bottom: 4px;
  font-size: 12px;
  font-weight: 800;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: #5d564d;
}
.sm-shop fieldset label {
  display: flex;
  align-items: center;
  gap: 10px;
  min-height: 34px;
  font-size: 14.5px;
  cursor: pointer;
}
.sm-shop input[type="checkbox"],
.sm-shop input[type="radio"] {
  width: 18px;
  height: 18px;
  margin: 0;
  accent-color: #c0392b;
}
.sm-shop__sizes {
  display: flex;
  gap: 16px;
}
.sm-shop__range {
  display: flex;
  justify-content: space-between;
  margin-top: 12px;
  font-size: 14px;
  font-weight: 700;
}
.sm-shop__slider {
  width: 100%;
  margin: 10px 0 0;
  accent-color: #c0392b;
}
.sm-shop__foot {
  display: grid;
  grid-template-columns: auto 1fr;
  gap: 8px;
  padding: 12px 18px 16px;
  border-top: 1px solid #ece8e2;
}
.sm-shop__clear,
.sm-shop__apply {
  min-height: 44px;
  padding: 0 14px;
  border-radius: 10px;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-shop__clear {
  border: 1.5px solid #cfc8bd;
  background: #fff;
  color: #1e1b18;
}
.sm-shop__apply {
  border: 0;
  background: #b03427;
  color: #fff;
}
.sm-shop button:focus-visible,
.sm-shop input:focus-visible {
  outline: 3px solid #1e1b18;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-shop__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-shop.is-open .sm-shop__panel {
    transition-property: transform;
  }
  .sm-shop__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

Notification slide-over

  • non-modal panel
  • aria-label update
  • unread state
Tasklane Q4 launch plan

A desktop slide-over from the right that leaves the app usable behind it, so there is no backdrop and no aria-modal; a click outside or Escape closes it. The bell carries the unread count in its accessible name, and Mark all as read updates the badge, the name and a status line together. Use position: fixed under the app header on a real site.

View code
.sm-note {
  position: relative;
  width: min(720px, 100%);
  height: 400px;
  overflow: hidden;
  border-radius: 18px;
  background: #f4f5f9;
  color: #181b2a;
  box-shadow: 0 14px 40px rgba(24, 27, 42, .12);
}
.sm-note a {
  color: #3a3fc4;
  font-weight: 600;
  text-decoration: none;
}
.sm-note a:hover {
  text-decoration: underline;
}
.sm-note__bar {
  position: relative;
  z-index: 5;
  display: flex;
  align-items: center;
  gap: 14px;
  height: 60px;
  padding: 0 12px 0 18px;
  background: #181b2a;
  color: #eef0fa;
}
.sm-note__brand {
  font-size: 17px;
  font-weight: 800;
}
.sm-note__crumb {
  overflow: hidden;
  color: #b6bad3;
  font-size: 13.5px;
  white-space: nowrap;
  text-overflow: ellipsis;
}
.sm-note__bell {
  position: relative;
  display: grid;
  place-items: center;
  flex: none;
  width: 44px;
  height: 44px;
  margin-left: auto;
  padding: 0;
  border: 0;
  border-radius: 10px;
  background: #2a2e44;
  color: inherit;
  cursor: pointer;
}
.sm-note__bell[aria-expanded="true"] {
  background: #eef0fa;
  color: #181b2a;
}
.sm-note__bell svg {
  fill: none;
  stroke: currentColor;
  stroke-width: 1.9;
  stroke-linejoin: round;
  stroke-linecap: round;
}
.sm-note__badge {
  position: absolute;
  top: 4px;
  right: 4px;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  background: #e5484d;
  color: #fff;
  font-size: 11px;
  font-weight: 800;
  line-height: 18px;
  text-align: center;
}
.sm-note__badge[hidden] {
  display: none;
}
.sm-note__board {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 12px;
  padding: 18px;
}
.sm-note__board div {
  display: grid;
  gap: 10px;
  align-content: start;
  padding: 10px;
  border-radius: 12px;
  background: #e8eaf2;
}
.sm-note__board i {
  height: 54px;
  border-radius: 8px;
  background: #fff;
}
.sm-note__panel {
  position: absolute;
  top: 60px;
  right: 0;
  bottom: 0;
  z-index: 4;
  display: flex;
  flex-direction: column;
  width: min(340px, 92%);
  padding: 14px 16px 16px;
  box-sizing: border-box;
  background: #fff;
  box-shadow: -14px 0 40px rgba(24, 27, 42, .18);
  transform: translateX(calc(100% + 40px));
  visibility: hidden;
}
.sm-note.is-open .sm-note__panel {
  transform: none;
  visibility: visible;
}
.sm-note__panel:focus {
  outline: none;
}
.sm-note__panel:focus-visible {
  outline: 3px solid #3a3fc4;
  outline-offset: -3px;
}
.sm-note__head {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: 6px;
}
.sm-note__title {
  margin: 0;
  font-size: 17px;
  font-weight: 800;
}
.sm-note__read {
  min-height: 36px;
  padding: 0 10px;
  border: 0;
  border-radius: 8px;
  background: #eceefc;
  color: #2f34a8;
  font: inherit;
  font-size: 13px;
  font-weight: 700;
  cursor: pointer;
}
.sm-note__status {
  min-height: 1.4em;
  margin: 4px 0;
  color: #1d7a45;
  font-size: 13px;
  font-weight: 600;
}
.sm-note__list {
  flex: 1;
  margin: 0;
  padding: 0;
  overflow-y: auto;
  list-style: none;
}
.sm-note__list li {
  position: relative;
  display: flex;
  gap: 10px;
  padding: 10px 14px 10px 0;
  border-bottom: 1px solid #eceef4;
}
.sm-note__list li.is-unread::after {
  content: "";
  position: absolute;
  top: 18px;
  right: 2px;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  background: #3a3fc4;
}
.sm-note__av {
  display: grid;
  place-items: center;
  flex: none;
  width: 34px;
  height: 34px;
  border-radius: 50%;
  background: var(--sm-note-c);
  color: #181b2a;
  font-size: 11.5px;
  font-weight: 800;
}
.sm-note__list p {
  margin: 0;
  font-size: 13.5px;
  line-height: 1.45;
}
.sm-note__list time {
  display: block;
  color: #5d6178;
  font-size: 12px;
}
.sm-note__close {
  min-height: 44px;
  margin-top: 10px;
  border: 1.5px solid #d3d6e4;
  border-radius: 10px;
  background: #fff;
  color: #181b2a;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-note button:focus-visible,
.sm-note a:focus-visible {
  outline: 3px solid #8b90ff;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-note__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s;
  }
  .sm-note.is-open .sm-note__panel {
    transition-property: transform;
  }
}

Right-to-left slide menu

  • dir="rtl"
  • inset-inline-start
  • mirrored translate

An Arabic bookshop menu. The panel is anchored with inset-inline-start, so it hugs the right edge under dir="rtl" without any extra rule, and only the translate needs mirroring: -100% in left-to-right, 100% in right-to-left. Padding, borders and the chevrons use logical properties too. Use position: fixed on a real site.

View code
.sm-rtl a {
  color: inherit;
  text-decoration: none;
}
.sm-rtl__bar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 14px;
  background: #0f4c5c;
  color: #fdf6e9;
}
.sm-rtl__toggle {
  display: grid;
  place-items: center;
  width: 44px;
  height: 44px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: #f2c14e;
  color: #0f4c5c;
  cursor: pointer;
}
.sm-rtl__toggle span,
.sm-rtl__toggle span::before,
.sm-rtl__toggle span::after {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 2px;
  background: currentColor;
}
.sm-rtl__toggle span {
  position: relative;
}
.sm-rtl__toggle span::before,
.sm-rtl__toggle span::after {
  content: "";
  position: absolute;
  inset-inline-start: 0;
}
.sm-rtl__toggle span::before {
  top: -6px;
}
.sm-rtl__toggle span::after {
  top: 6px;
  width: 11px;
}
.sm-rtl__logo {
  font-size: 20px;
  font-weight: 700;
}
.sm-rtl__backdrop {
  position: absolute;
  inset: 0;
  z-index: 3;
  background: rgba(6, 30, 36, .55);
  opacity: 0;
  visibility: hidden;
}
.sm-rtl__panel {
  position: absolute;
  inset-block: 0;
  inset-inline-start: 0;
  z-index: 4;
  width: min(290px, 84%);
  padding-block: 12px 20px;
  padding-inline: 22px 18px;
  box-sizing: border-box;
  background: #fdf6e9;
  color: #133038;
  transform: translateX(-100%);
  visibility: hidden;
}
.sm-rtl[dir="rtl"] .sm-rtl__panel {
  transform: translateX(100%);
}
.sm-rtl.is-open .sm-rtl__backdrop {
  opacity: 1;
  visibility: visible;
}
.sm-rtl.is-open .sm-rtl__panel {
  transform: none;
  visibility: visible;
  box-shadow: 0 0 40px rgba(6, 30, 36, .3);
}
.sm-rtl__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding-block-end: 10px;
  border-block-end: 2px solid #f2c14e;
}
.sm-rtl__title {
  margin: 0;
  font-size: 18px;
  font-weight: 700;
}
.sm-rtl__close {
  min-height: 44px;
  padding-inline: 14px;
  border: 1.5px solid #0f4c5c;
  border-radius: 999px;
  background: none;
  color: #0f4c5c;
  font: inherit;
  font-size: 14px;
  font-weight: 700;
  cursor: pointer;
}
.sm-rtl__list {
  margin: 8px 0 0;
  padding: 0;
  list-style: none;
}
.sm-rtl__list a {
  display: flex;
  align-items: center;
  justify-content: space-between;
  min-height: 48px;
  border-block-end: 1px solid #ecdfc6;
  font-size: 17px;
  font-weight: 600;
}
.sm-rtl__list a::after {
  content: "";
  width: 7px;
  height: 7px;
  border-block-start: 2px solid #b07d12;
  border-inline-end: 2px solid #b07d12;
  transform: rotate(45deg);
}
.sm-rtl[dir="rtl"] .sm-rtl__list a::after {
  transform: rotate(-45deg);
}
.sm-rtl__list a:hover {
  color: #0f6f86;
}
.sm-rtl button:focus-visible,
.sm-rtl a:focus-visible {
  outline: 3px solid #b07d12;
  outline-offset: 2px;
}
@media (prefers-reduced-motion: no-preference) {
  .sm-rtl__panel {
    transition: transform .35s cubic-bezier(.2, .8, .2, 1), visibility .35s, box-shadow .35s;
  }
  .sm-rtl.is-open .sm-rtl__panel {
    transition-property: transform, box-shadow;
  }
  .sm-rtl__backdrop {
    transition: opacity .3s ease, visibility .3s;
  }
}

What Is a Slide Menu?

Anything that stays off-screen until a trigger slides it in from one edge, then slides back out when dismissed, counts as a slide menu. While it’s closed, the page content keeps the full viewport, which is the whole appeal on small screens.

Bootstrap calls its version an off-canvas component (Bootstrap 5.3 documentation). Mobile app design gave us navigation drawer. Slide-out menu and side menu are the plain-English names for the same panel.

People often call the whole thing a hamburger menu, but the hamburger is only the three-bar button. The panel it opens is the slide menu.

App designers call that panel a drawer, and the drawer pattern in UI design sets visitor expectations. It hugs one edge and scrolls on its own, and visitors can dismiss it whenever they want.

Typical contents are secondary links and grouped sections. Justinmind’s slide menu design guide uses account links (profile, settings, logout) and mail folders (inbox, sent, drafts) as grouping examples.

It isn’t a dropdown, since a dropdown opens in place under its trigger. It isn’t a fixed sidebar either, because a sidebar never hides.

Slide Menu Types

The edge a slide menu enters from, and whether it covers the page or pushes it aside, shape most of how it behaves. Link depth is the other variable, and it matters more on bigger sites.

Origin Edge

Bootstrap 5.3 documents a placement for each edge of the screen, and none is applied by default.

  • offcanvas-start is the left edge
  • offcanvas-end sits on the right
  • offcanvas-top drops in from the top
  • offcanvas-bottom rises from the bottom

Each placement anchors to one edge of the viewport and slides along that edge’s axis. Left and right panels suit long link lists because they scroll vertically, while top and bottom panels fit short lists.

Justinmind adds a consistency rule. Pick left or right and keep that side across the whole product.

Overlay or Push

Overlay panels sit on top of the page while a backdrop dims whatever is underneath. The page stays where it is. Bootstrap’s offcanvas works this way, with a clickable backdrop on by default.

A push menu does something different. The page content slides sideways by the panel width as the panel enters, so two things are moving at once and the transition reads as a layout change instead of a layer opening.

Overlay is the safer default. It leaves the page layout untouched, so nothing reflows or loses its scroll position while the menu is open.

Full-Screen and Multi-Level Menus

A full-screen slide menu covers the entire viewport, which gives large touch targets and no backdrop to manage.

Multi-level menus swap the current list for the submenu instead of expanding it downward, and a back button returns to the previous level. Documentation for the Extras for Elementor and PowerPack slide menu widgets describes this drill-down. The reasoning is practical, since submenus that expand inside a panel create a scroll area inside a scroll area.

Every level adds a tap and a back step, so keep the hierarchy shallow.

When to Use a Slide Menu (and When Visible Navigation Wins)

Use a slide menu for secondary links on narrow screens, and keep primary navigation visible on every screen size.

The evidence behind that rule is Nielsen Norman Group’s 2016 quantitative study of hidden navigation. It tested hamburger-style menus against visible navigation and against combo navigation, where some links are visible and some are hidden.

179 participants completed tasks on 6 live websites, on both phones and desktops. Content discoverability dropped by more than 20% with hidden navigation. Desktop users were at least 39% slower to finish tasks when navigation was hidden than with visible or combo navigation, and mobile users were 15% slower than with combo navigation.

Design roundups often present a slide menu as a space-saving main menu for desktop sites. The NN/g data points the other way, because hiding navigation hurt the desktop experience more than the phone experience.

One limit on that evidence. The study hid the main navigation under an icon, and secondary links moved into a slide menu sit outside what it measured. That’s why the rule here keeps primary links visible.

Following NN/g’s recommendations, a desktop site should show its top-level links across the top or down the left side, with nothing hidden. On mobile with 4 or fewer top-level links, show all of them as visible links. With more than 4, hide some in a slide menu and keep the rest visible.

On wide screens, a visible sidebar navigation saves the same horizontal space as a slide menu without the extra click.

Justinmind’s guide points the same way. Its hybrid pattern pairs a bottom navigation bar for the everyday essentials with a slide menu for less frequently used options. The usability penalty falls hardest on content with no in-page link, so link to key pages from the content itself.

How Visitors Open and Close a Slide Menu

Most visitors open the panel with the hamburger button. An edge swipe works too, and on desktop a hover can do it, but neither is reliable enough to stand alone.

The button is the trigger that always works. Swipe gestures are invisible, so a visible button has to sit next to them (NN/g lists low salience as the first reason hidden navigation underperforms).

Hover fails on touch screens, and keyboard users cannot hover at all. A hover trigger needs a click or focus equivalent.

Most builds draw the three bars with CSS instead of loading an icon file, and collections of CSS hamburger menus show common ways to draw and animate the icon. Whatever the drawing looks like, the trigger should be a real button element.

Closing has more routes. Bootstrap 5.3 recommends a header with a dismiss action whenever possible, so a close button comes first. Clicking the backdrop closes the panel by default in Bootstrap, and you can switch to a static backdrop that ignores outside clicks.

Tapping a link inside the panel is a case people forget. Single-page apps and in-page anchors must close the panel in code, because no page load does it for them.

Then there are the platform gestures. MDN’s dialog documentation lists Esc on desktop and a back or dismiss gesture on mobile as platform-specific close actions.

Every slide menu needs at least one visible close control. A backdrop click alone cannot be operated from a keyboard.

How to Build a Slide Menu with HTML, CSS and JavaScript

The first four steps below are plain HTML and CSS. The toggle and the backdrop need JavaScript.

  1. Write the markup. That’s a button with aria-controls pointing at the panel id and aria-expanded set to false, followed by a nav element holding the link list.
  2. Position the panel with fixed positioning, anchored to the left edge from top to bottom, with a set width.
  3. Move it off-screen by applying transform: translateX(-100%) in the closed state, and add a transform transition for the slide.
  4. Add the open state. An .open class resets the transform to translateX(0).
  5. Wire up one click handler that adds or removes .open on the panel and updates aria-expanded on the button.
  6. Add the backdrop, a fixed layer behind the panel that closes it on click, and lock body scrolling while the panel is open.

The native dialog element is another route. Opened with showModal(), it makes the rest of the page inert, closes on Esc, exposes a ::backdrop pseudo-element for the dimmed layer, and moves focus to the first focusable element inside (MDN). That replaces the backdrop layer and the Escape and inert handling you would otherwise write by hand.

Browsers have supported the element across the board since March 2022. Animating it takes extra CSS, since MDN’s transition recipe needs @starting-style plus transition-behavior: allow-discrete on display and overlay. Firefox and Safari don’t support overlay yet, so the close animation can be cut short there.

CSS-Only Version and Its Limits

A slide menu works without JavaScript. The usual CSS-only mechanisms are a hidden checkbox paired with a label, or the :target selector, and SitePoint’s pure-CSS off-screen menu tutorial builds one with the checkbox approach.

CSS alone can’t update aria-expanded on the toggle. It can’t move focus into the panel or return it on close, and it can’t close on Escape. The :target variant also writes a fragment into the URL, which some people will find annoying.

CSS-only suits a prototype or a small brochure site. Anything that keyboard and screen reader users must operate reliably needs the JavaScript version.

Which CSS Property Should Animate a Slide Menu?

Animate transform. A translateX transition can run off the main thread, while changes to left, margin or width force layout work on the main thread.

MDN’s animation performance guide names transform as the most common property that avoids reflow and repaint. When the panel sits on its own layer, the GPU handles the motion, which helps most on mobile.

PropertyBrowser costVerdict for a slide menu
transform: translateXNo reflow or repaint, GPU-eligibleDefault choice
left, right or marginTriggers reflow on the main threadAvoid
width (jQuery animate style)Reflows the panel and its content each frameLegacy only

The width approach is the 2010 pattern. Smashing Magazine’s jQuery sliding menu tutorial from that year animates a container from 0px to a set width with jQuery’s animate method, which leaves the browser re-laying out the panel on every step.

Sources also disagree on CSS versus JavaScript. MDN reports near-identical frame rates in Firefox when both run on the main thread, while GSAP and Velocity.js claim an edge over native CSS.

MDN’s own resolution is that CSS wins once the property is compositor-friendly, because the sampling leaves the main thread. So the property choice matters more than the animation tool.

Bootstrap 5.3 ships its offcanvas with a 0.3-second ease-in-out transform transition, with the duration set by a Sass variable. Its docs also say margin and translate cannot be applied to the .offcanvas element itself, because of how CSS handles the animation. Apply them to an inner element and keep .offcanvas as the independent wrapper.

MDN finds no performance difference between CSS transitions and keyframe animations, so open and close fit a plain transition. CSS keyframes earn their place only when the motion needs intermediate steps, such as a staggered reveal of the menu links.

How to Make a Slide Menu Accessible

An overlay slide menu with a backdrop should follow the modal dialog pattern from the W3C’s ARIA Authoring Practices Guide (APG). Applied to a slide menu, the pattern asks for the following.

  • The panel gets role dialog and aria-modal set to true
  • A visible title referenced by aria-labelledby, or an aria-label that names the panel
  • Focus moves into the panel on open and goes back to the toggle on close
  • Tab and Shift+Tab loop inside the panel
  • A visible close button sits in the tab sequence

Bootstrap 5.3 adds role dialog through JavaScript and expects aria-labelledby to point at the offcanvas title. Its keyboard option closes the panel on Esc by default.

Marking the panel modal with ARIA is a commitment. The APG warns that users of some assistive technologies lose access to everything outside a dialog marked modal, so the flag belongs only where code blocks all interaction outside the panel and the styling visibly obscures the page.

A push menu that leaves the page visible and clickable fails both tests, so it should not carry aria-modal.

From a web accessibility audit’s point of view, a focus loop looks a lot like a trap. WCAG 2.2 criterion 2.1.2 (No Keyboard Trap, Level A) accepts constrained focus inside a modal as long as the user knows how to leave, and the W3C names Esc as a common standard exit method.

A custom panel also has to silence the page behind it. Set inert on the page wrapper while the menu is open, never on the panel itself.

MDN documents inert as removing content from the tab order and the accessibility tree, with cross-browser availability since April 2023. It adds no visual cue, so the dimmed backdrop does that job.

Motion is the last piece. A panel sliding in counts as motion animation under WCAG 2.3.3 (Animation from Interactions, Level AAA), because the criterion treats an element that moves into place as animated.

MDN adds that panning large objects can trigger vestibular reactions, and prefers-reduced-motion has worked across browsers since January 2020.

The fix is a reduce branch that swaps the slide for a fade. WCAG’s definition does not count opacity changes as motion animation, and Bootstrap’s offcanvas already keys its animation to the prefers-reduced-motion media query.

How a Slide Menu Should Behave Across Screen Sizes

Show the slide menu below a breakpoint and visible navigation above it. Bootstrap and Material UI both build that split into their panel components.

In Bootstrap 5.3, replace .offcanvas with .offcanvas-lg (or sm, md, xl, xxl) and the panel turns into inline content above that breakpoint. Responsive offcanvas classes arrived in Bootstrap 5.2.0. The toggle button needs a matching display utility so it disappears at the same breakpoint, and the close button needs an explicit data-bs-target.

Material UI takes the other approach, with the temporary Drawer variant on small screens and the permanent variant on wider ones. Its docs call the permanent drawer the recommended default for desktop. The persistent variant is acceptable for every size larger than mobile, though it isn’t recommended for apps with a multi-level hierarchy, and when it sits outside the page grid, it forces the page content to resize as it opens.

Custom builds do the same with media queries. At the breakpoint, reset the panel to static positioning, hide the toggle and drop the backdrop.

Crossing the breakpoint with the panel open is a common bug. Close the panel and clear the backdrop, the scroll lock and any inert state before the desktop layout takes over.

Pick the breakpoint from the link count, not from a device list. It’s the width where the visible bar starts wrapping or truncating labels.

A mobile-first design workflow starts with the slide menu and adds the visible bar at that width. NN/g’s warning applies here, since mobile-first should not turn into mobile-only.

Which Slide Menu Library or Component to Use

Use the component your stack already ships. That means Bootstrap’s offcanvas on Bootstrap sites, Material UI’s Drawer in React apps, and mmenu.js only when the menu needs sliding submenus.

Sites already built on Bootstrap get placement options, a backdrop, Esc handling and body scroll control without another dependency.

OptionBest fitProCon
Bootstrap OffcanvasBootstrap 5 sitesFour placements, backdrop and Esc handling includedNo built-in drill-down
Material UI DrawerReact appsTemporary, persistent and permanent variantsSwipeable version adds 2 kB gzipped
mmenu.jsMulti-level mobile menusSliding submenus, back button and searchfield add-onsCommercial use costs $15 or $150
SlinkyLegacy jQuery sitesLight drill-down navigationNeeds jQuery, last release 4.2.1 in 2019
SlidebarsLegacy jQuery sitesPanels on any side, unlimited instances (v2)v2 breaks compatibility with 0.x

The mmenu.js license has a split. Personal and non-profit sites use it free, a single commercial website costs $15, and unlimited commercial sites cost $150. The same author offers Mmenu Light as a lighter alternative.

Accessibility-first options exist too. js-offcanvas v2 is a dependency-free vanilla JavaScript class with focus trapping and focus restore, and react-aria-offcanvas ships with body lock, focus trap and focus return switched on by default.

For a single-level menu I’d skip the library entirely. A custom build or the native dialog element beats loading jQuery for a slide effect, and Slinky’s last npm release, 4.2.1, dates to July 2019.

What Breaks in Slide Menus and How to Fix It

Most slide menu bugs come from the same few places.

Focusable links in a closed panel

A panel moved off-screen with transform stays in the tab order and the accessibility tree, so keyboard users tab into links they cannot see.

Set inert on the closed panel, or switch it to visibility: hidden once the close transition ends. MDN notes that inert also works on elements meant to stay offscreen, not only on background content.

Page scrolling behind the panel

Bootstrap disables body scrolling while an offcanvas and its backdrop are visible, and exposes data-bs-scroll to allow body scrolling instead. A custom build sets overflow: hidden on the body when the panel opens and removes it on close.

Stacking collisions

A sticky navigation bar often paints over the panel because the panel sits inside a different stacking context.

Mount the panel directly under the body, outside any ancestor that sets transform. A transformed ancestor becomes the containing block for fixed descendants, which is also why push menus that transform the page wrapper break fixed headers.

A panel taller than the visible screen

A height of 100vh sizes the panel to the large viewport, so on mobile browsers with a retractable address bar the bottom links sit behind the toolbar.

MDN’s <length> reference for CSS viewport units (last updated July 2026) states that vh currently equals lvh. It lists svh as the safe size and dvh as the live size.

Anchoring the panel with top and bottom insets instead of a height sidesteps the whole problem. If you do need a fixed size, use svh. Avoid dvh for long panels, since MDN flags resizing during scroll as a performance hit.

Swipe gestures against the browser

Material UI’s documentation says the iOS swipe-to-go-back gesture interferes with the drawer’s edge discovery, so its own docs site turns discovery off on iOS.

The same docs warn that some low-end phones cannot follow the finger at 60 FPS and offer a disableBackdropTransition prop. Keep the hamburger button as the primary trigger and treat edge swipe as a bonus.

Slide Menu FAQ

How wide should a slide menu be?

Bootstrap’s offcanvas defaults to 400px wide for left and right panels and 30vh tall for top and bottom ones. That suits tablets and desktops.

On narrow phones, cap the width at 100% of the viewport so the panel never overflows the screen.

Does a slide menu hurt SEO?

No. Links inside a closed panel stay in the page’s HTML, and Google’s Gary Illyes said in 2016 that content hidden for user experience should get full weight under mobile-first indexing, as Search Engine Journal reported.

Keep the links as real anchor elements in the initial markup, not injected after a click.

Can a slide menu hold more than links?

It can. Bootstrap’s docs say an offcanvas panel can hold just about any component or custom element, and mmenu.js adds navbars for logos, breadcrumbs and tabs plus a searchfield add-on.

Keep the panel scannable, because everything inside it competes with the navigation links.

Can one page carry two slide menus, one on each side?

Mostly, though some components limit it. Bootstrap allows several offcanvas elements but shows one at a time, while Slidebars 2.0 supports an unlimited number of off-canvas instances on any side.

Give each panel its own toggle and its own aria-labelledby title.

How does a slide menu handle right-to-left languages?

mmenu.js lists right-to-left support and translations as built-in features.

In a custom build, anchor the panel with inset-inline-start instead of left, and mirror the translateX direction under dir="rtl" so the panel enters from the reading-start edge.

How to Test a Slide Menu Before Launch

Run the checks in order of how badly each failure blocks visitors. Keyboard goes first, then reduced motion, then a physical phone.

  • Keyboard only, covering Tab, Shift+Tab, Esc and focus return
  • Reduced-motion emulation in browser developer tools
  • One real phone, to check the address bar, edge swipe and scroll lock

The order follows conformance level. A keyboard trap fails WCAG 2.1.2 at Level A, while motion triggered by interaction sits at Level AAA under 2.3.3, so a broken Esc key outranks an unreduced animation.

The phone pass costs an extra device, but it exposes the toolbar overlap and the iOS back-swipe conflict that desktop browsers hide.

Recheck the viewport step if browsers change what vh resolves to, since MDN currently lists it as equal to lvh.

Once the panel passes, the same keyboard and focus checks apply to the site’s other components, and a web accessibility checklist covers the rest.