Navigation

Drawer Nav

A mobile drawer menu: it slides in from the left, dims the content, and closes on a tap outside, on Escape or via the close button.

  • sidebar
  • drawer
  • mobile
  • overlay

Preview

1440pxHost theme

Use it with AI

  1. 1. Copy the link.
  2. 2. Write to your agent in your own words and drop the link into the sentence.
  3. 3. The agent opens the link and installs the component from the registry.

put this in the header: https://vibeui.ru/c/sidebar-009?lang=en

Северный порт

Содержимое страницы. Ящик выезжает поверх него и затемняет всё, что под ним, — нажатие по затемнению закрывает меню.

Show the full instructions

What the link says, spelled out. Use it when your agent cannot open links — paste this text instead.

# Install and place "sidebar-009" (Drawer Nav) from VibeUI

## 1. Install first — do not skip, do not recreate
Run this exact command before writing any code:
npx shadcn@latest add https://vibeui.ru/r/sidebar-009.json

Registry item: https://vibeui.ru/r/sidebar-009.json
Installs to: components/vibeui/sidebar-009.tsx (the exact path follows this project's components.json aliases).
npm dependencies: none.
Registry dependencies: none.

Install it from the registry. Do not recreate it from the description,
do not substitute a similar component from another library, and do not
rewrite it to match the project's existing style.

## 2. What it is
A mobile drawer menu: it slides in from the left, dims the content, and closes on a tap outside, on Escape or via the close button.

A drawer menu with a scrim: the closed panel is inert, focus moves to the close button and returns to the burger. Zero dependencies, one file.

## 3. How to use it
import { Sidebar009 } from "@/components/vibeui/sidebar-009"

<Sidebar009
  title="Menu"
  activeLabel="Orders"
  items={[{ label: "Catalogue", href: "/catalog" }, { label: "Orders", href: "/orders" }]}
/>

Read the installed file for the full prop list.

## 4. Where to place it
This is a small inline component. Put it exactly where the user asked,
inside the existing markup. Do not create a new page, section or
wrapper for it. If a similar control already sits in that spot,
replace it instead of adding a second one.

Placement: ___
(The user fills this line in. If it is still blank, ask where to put it
instead of guessing.)

## 5. Keep exactly as installed
- the local --vibeui-sidebar-009-* palette — do not swap it for your theme tokens
- inert on the closed panel: otherwise Tab falls into invisible links
- the scrim as a tap target — on a phone it is the main way to close the drawer
- returning focus to the burger on close: otherwise focus is left nowhere
- closing on Escape — without it the drawer becomes a keyboard trap
- aria-expanded on the burger and a label on the close button: the state must be audible

## 6. You may change
- the items through items and the current one through activeLabel
- the drawer heading through title and the burger label through openLabel
- the accent colour through accent and the scrim density in --vibeui-sidebar-009-scrim
- the panel width in the width rule of [data-part="drawer"]

## 7. Rules
- The drawer lives inside the block: for a real mobile menu switch absolute to fixed and add page scroll locking.
- Focus is not cycled inside the panel: a full focus trap needs its own Tab handler.
- The scrim does not close the drawer on swipe: the component handles no gestures.
- Do not lift the CSS variables into globals.css: the component has to stay a single file.

## 8. Verify
- it renders with no console errors;
- it looks like the preview on the VibeUI page you copied this from;
- if it does not, you changed something listed in section 5 — put it back.

For developers

npx shadcn@latest add https://vibeui.ru/r/sidebar-009.json
https://vibeui.ru/r/sidebar-009.json

Компонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-sidebar-009-*. Клиентский ("use client") — состояние ящика держит useState. Затемнение это кнопка с aria-hidden и tabIndex −1: оно ловит нажатие мимо панели, но не попадает в порядок обхода. Закрытый ящик помечен inert, поэтому таб не проваливается в невидимые ссылки; при открытии фокус уезжает на крестик, а при закрытии возвращается на кнопку-бургер. Escape ловится на корне блока. Ящик позиционирован внутри блока, а не относительно окна: демонстрация не захватывает страницу.

Component source

The same file your agent installs. Here in case you would rather copy it by hand.

"use client"

import { useEffect, useRef, useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Sidebar009Item = {
  label: string
  href?: string
}

export type Sidebar009Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  items?: Sidebar009Item[]
  activeLabel?: string
  title?: string
  openLabel?: string
  accent?: string
}

// Идея компонента: меню-ящик выезжает слева и затемняет то, что под ним.
// Затемнение здесь не декорация: оно ловит нажатие мимо панели и закрывает
// ящик — на телефоне это главный способ выйти. Закрытый ящик помечен inert,
// поэтому таб не проваливается в невидимые ссылки, а при открытии фокус
// уезжает на кнопку закрытия, чтобы Escape и Tab работали сразу.
const STYLES = `
:where([data-vibeui-block="sidebar-009"]){
--vibeui-sidebar-009-bg:oklch(1 0 0);
--vibeui-sidebar-009-fg:oklch(0.25 0.016 265);
--vibeui-sidebar-009-muted:oklch(0.55 0.014 265);
--vibeui-sidebar-009-border:oklch(0.91 0.006 265);
--vibeui-sidebar-009-accent:oklch(0.55 0.19 262);
--vibeui-sidebar-009-scrim:oklch(0.2 0.02 265 / 45%);
--vibeui-sidebar-009-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="sidebar-009"]{
position:relative;overflow:hidden;
display:flex;flex-direction:column;
width:100%;max-width:20rem;height:16rem;box-sizing:border-box;
background:var(--vibeui-sidebar-009-bg);color:var(--vibeui-sidebar-009-fg);
border:1px solid var(--vibeui-sidebar-009-border);border-radius:0.875rem;
font-family:var(--vibeui-sidebar-009-font);
}
[data-vibeui-block="sidebar-009"] [data-part="bar"]{
display:flex;align-items:center;gap:0.625rem;
padding:0.625rem 0.75rem;border-bottom:1px solid var(--vibeui-sidebar-009-border);
}
[data-vibeui-block="sidebar-009"] [data-part="burger"],
[data-vibeui-block="sidebar-009"] [data-part="close"]{
appearance:none;cursor:pointer;flex:none;
display:inline-flex;align-items:center;justify-content:center;
width:2rem;height:2rem;padding:0;
border:1px solid var(--vibeui-sidebar-009-border);border-radius:0.5rem;
background:var(--vibeui-sidebar-009-bg);color:var(--vibeui-sidebar-009-fg);
}
[data-vibeui-block="sidebar-009"] [data-part="burger"]:focus-visible,
[data-vibeui-block="sidebar-009"] [data-part="close"]:focus-visible{outline:2px solid var(--vibeui-sidebar-009-accent);outline-offset:2px}
[data-vibeui-block="sidebar-009"] svg{width:0.875rem;height:0.875rem;display:block}
[data-vibeui-block="sidebar-009"] [data-part="brand"]{font-size:0.875rem;font-weight:650}
[data-vibeui-block="sidebar-009"] [data-part="page"]{
flex:1;padding:0.75rem;font-size:0.8125rem;line-height:1.5;color:var(--vibeui-sidebar-009-muted);
}
/* Затемнение ловит нажатие мимо панели: на телефоне это главный выход. */
[data-vibeui-block="sidebar-009"] [data-part="scrim"]{
appearance:none;border:0;padding:0;cursor:pointer;
position:absolute;inset:0;z-index:1;
background:var(--vibeui-sidebar-009-scrim);
opacity:0;visibility:hidden;transition:opacity .2s ease,visibility .2s ease;
}
[data-vibeui-block="sidebar-009"][data-open="true"] [data-part="scrim"]{opacity:1;visibility:visible}
[data-vibeui-block="sidebar-009"] [data-part="drawer"]{
position:absolute;inset:0 auto 0 0;z-index:2;
display:flex;flex-direction:column;gap:0.625rem;
width:12rem;box-sizing:border-box;padding:0.625rem;
background:var(--vibeui-sidebar-009-bg);
border-right:1px solid var(--vibeui-sidebar-009-border);
box-shadow:0 0 24px oklch(0.2 0.02 265 / 18%);
transform:translateX(-100%);transition:transform .22s ease;
}
[data-vibeui-block="sidebar-009"][data-open="true"] [data-part="drawer"]{transform:translateX(0)}
[data-vibeui-block="sidebar-009"] [data-part="head"]{display:flex;align-items:center;justify-content:space-between;gap:0.5rem}
[data-vibeui-block="sidebar-009"] [data-part="title"]{margin:0;font-size:0.8125rem;font-weight:700;letter-spacing:0.02em}
[data-vibeui-block="sidebar-009"] ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:0.125rem}
[data-vibeui-block="sidebar-009"] [data-part="drawer"] a{
display:block;padding:0.4375rem 0.5rem;border-radius:0.5rem;
color:var(--vibeui-sidebar-009-muted);text-decoration:none;font-size:0.875rem;line-height:1.3;
}
[data-vibeui-block="sidebar-009"] [data-part="drawer"] a:hover{background:oklch(0.55 0.02 265 / 8%);color:var(--vibeui-sidebar-009-fg)}
[data-vibeui-block="sidebar-009"] [data-part="drawer"] a:focus-visible{outline:2px solid var(--vibeui-sidebar-009-accent);outline-offset:-2px}
[data-vibeui-block="sidebar-009"] [data-part="drawer"] a[aria-current="page"]{
background:color-mix(in oklab,var(--vibeui-sidebar-009-accent) 14%,transparent);
color:var(--vibeui-sidebar-009-fg);font-weight:600;
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="sidebar-009"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ITEMS: Sidebar009Item[] = [
  { label: "Главная", href: "#" },
  { label: "Каталог", href: "#" },
  { label: "Заказы", href: "#" },
  { label: "Избранное", href: "#" },
  { label: "Поддержка", href: "#" },
]

/**
 * Мобильное меню-ящик: выезжает слева, затемнение закрывает по нажатию мимо.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Sidebar009({
  items = DEFAULT_ITEMS,
  activeLabel = "Заказы",
  title = "Меню",
  openLabel = "Открыть меню",
  accent,
  className,
  style,
  ...props
}: Sidebar009Props) {
  const [open, setOpen] = useState(false)
  const closeButton = useRef<HTMLButtonElement>(null)
  const burger = useRef<HTMLButtonElement>(null)

  useEffect(() => {
    if (open) closeButton.current?.focus()
  }, [open])

  const palette = {
    ...(accent ? { "--vibeui-sidebar-009-accent": accent } : null),
    ...style,
  } as CSSProperties

  function close() {
    setOpen(false)
    burger.current?.focus()
  }

  return (
    <>
      <style href="vibeui-sidebar-009" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="sidebar-009"
        data-open={open}
        className={className}
        style={palette}
        onKeyDown={(event) => {
          if (event.key === "Escape" && open) close()
        }}
      >
        <div data-part="bar">
          <button
            type="button"
            data-part="burger"
            ref={burger}
            aria-expanded={open}
            aria-label={openLabel}
            onClick={() => setOpen(true)}
          >
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
              <path
                d="M2 4h12M2 8h12M2 12h12"
                strokeWidth="1.6"
                strokeLinecap="round"
              />
            </svg>
          </button>
          <span data-part="brand">Северный порт</span>
        </div>
        <p data-part="page">
          Содержимое страницы. Ящик выезжает поверх него и затемняет всё, что
          под ним, — нажатие по затемнению закрывает меню.
        </p>
        <button
          type="button"
          data-part="scrim"
          tabIndex={-1}
          aria-hidden="true"
          onClick={close}
        />
        <nav data-part="drawer" aria-label={title} inert={!open}>
          <div data-part="head">
            <h3 data-part="title">{title}</h3>
            <button
              type="button"
              data-part="close"
              ref={closeButton}
              aria-label="Закрыть меню"
              onClick={close}
            >
              <svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
                <path
                  d="m4 4 8 8M12 4l-8 8"
                  strokeWidth="1.6"
                  strokeLinecap="round"
                />
              </svg>
            </button>
          </div>
          <ul>
            {items.map((item) => (
              <li key={item.label}>
                <a
                  href={item.href}
                  aria-current={item.label === activeLabel ? "page" : undefined}
                  onClick={close}
                >
                  {item.label}
                </a>
              </li>
            ))}
          </ul>
        </nav>
      </div>
    </>
  )
}