Navigation

Nested Menubar

A menubar with nested submenus that fly out sideways on hover and on focus — without a single line of JS.

  • menubar
  • submenu
  • focus-within
  • no-js

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/menubar-007?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 "menubar-007" (Nested Menubar) 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/menubar-007.json

Registry item: https://vibeui.ru/r/menubar-007.json
Installs to: components/vibeui/menubar-007.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 menubar with nested submenus that fly out sideways on hover and on focus — without a single line of JS.

A menubar with two-level lists: an item with an arrow opens a submenu sideways. :hover and :focus-within do the opening, so the nested menu is reachable with a mouse and with a keyboard, and the component holds no state at all. Zero dependencies, one file, no client JS.

## 3. How to use it
import { Menubar007 } from "@/components/vibeui/menubar-007"

<Menubar007
  menus={[
    {
      label: "File",
      items: [{ label: "Export", items: [{ label: "To PDF" }] }],
    },
  ]}
/>

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-menubar-007-* palette — do not swap it for your theme tokens (bg-popover, border-border and the like)
- :focus-within alongside :hover: hover alone puts the submenu out of reach for keyboard users
- aria-haspopup="menu" on the parent item — otherwise a screen reader announces it as a plain command
- the arrow on the right of a parent item: without it the nesting is invisible until you hover
- the shared name attribute on <details>: it is how sections close one another
- positioning the submenu from the item row (left:100%) rather than the menu edge — otherwise it drifts away from its parent

## 6. You may change
- the menus array and the nested items on each entry
- the handlers on the leaf items
- the accent through the accent prop — it colours the focus ring
- the width of the menu and the submenu

## 7. Rules
- Do not go deeper than two levels: a third level is nearly impossible to hit with a mouse.
- Near the right edge of the screen the submenu overflows: add a mirrored left-hand opening for your layout.
- Escape does not close a <details>: that is the element's behaviour, not a bug.
- 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/menubar-007.json
https://vibeui.ru/r/menubar-007.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-menubar-007-*. Серверный: хуков нет. Разделы — <details> с общим атрибутом name, поэтому открыт всегда один. Подменю показывается правилом :hover и :focus-within у своей строки: фокус на родительском пункте уже раскрывает список, и в него можно уйти табуляцией, поэтому вложенность доступна и без мыши. Пункт с продолжением помечен aria-haspopup и стрелкой из бордюров.

Component source

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

import type { CSSProperties } from "react"

export type Menubar007Item = {
  label: string
  keys?: string
  items?: { label: string; keys?: string }[]
}

export type Menubar007Menu = {
  label: string
  items: Menubar007Item[]
}

export type Menubar007Props = {
  menus?: Menubar007Menu[]
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: меню с вложенными подменю, которые раскрываются вбок и без
// JS. Подменю показывает :focus-within у своего пункта — значит, оно доступно и
// мышью, и с клавиатуры: фокус на родительском пункте уже раскрывает список, и
// в него можно уйти табуляцией. Стрелка справа помечает пункт с продолжением.
const STYLES = `
:where([data-vibeui-block="menubar-007"]){
--vibeui-menubar-007-bg:oklch(1 0 0);
--vibeui-menubar-007-fg:oklch(0.24 0.014 265);
--vibeui-menubar-007-muted:oklch(0.58 0.014 265);
--vibeui-menubar-007-border:oklch(0.9 0.006 265);
--vibeui-menubar-007-hover:oklch(0.55 0.02 265 / 10%);
--vibeui-menubar-007-accent:oklch(0.55 0.2 262);
--vibeui-menubar-007-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="menubar-007"]{
box-sizing:border-box;width:100%;max-width:30rem;padding:0.25rem;
display:flex;align-items:center;gap:0.125rem;
background:var(--vibeui-menubar-007-bg);color:var(--vibeui-menubar-007-fg);
border:1px solid var(--vibeui-menubar-007-border);border-radius:0.625rem;
font-family:var(--vibeui-menubar-007-font);
}
[data-vibeui-block="menubar-007"] [data-part="slot"]{position:relative}
[data-vibeui-block="menubar-007"] [data-part="trigger"]{
list-style:none;cursor:pointer;display:flex;align-items:center;
height:1.875rem;padding:0 0.625rem;border-radius:0.4375rem;font-size:0.8125rem;
}
[data-vibeui-block="menubar-007"] [data-part="trigger"]::-webkit-details-marker{display:none}
[data-vibeui-block="menubar-007"] [data-part="trigger"]:hover{background:var(--vibeui-menubar-007-hover)}
[data-vibeui-block="menubar-007"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-menubar-007-accent);outline-offset:-2px}
[data-vibeui-block="menubar-007"] [data-part="slot"][open] > [data-part="trigger"]{background:var(--vibeui-menubar-007-hover)}
[data-vibeui-block="menubar-007"] [data-part="menu"]{
position:absolute;top:calc(100% + 0.375rem);left:0;z-index:30;
min-width:12.5rem;padding:0.25rem;box-sizing:border-box;margin:0;list-style:none;
background:var(--vibeui-menubar-007-bg);
border:1px solid var(--vibeui-menubar-007-border);border-radius:0.625rem;
box-shadow:0 16px 36px -18px oklch(0.2 0.03 265 / 45%);
}
[data-vibeui-block="menubar-007"] [data-part="row"]{position:relative}
[data-vibeui-block="menubar-007"] [data-part="item"]{
display:flex;align-items:center;justify-content:space-between;gap:1.25rem;
width:100%;min-height:1.875rem;padding:0 0.5rem;box-sizing:border-box;
appearance:none;border:0;background:none;cursor:pointer;border-radius:0.4375rem;
font:inherit;font-size:0.8125rem;color:inherit;text-align:left;
}
[data-vibeui-block="menubar-007"] [data-part="item"]:hover{background:var(--vibeui-menubar-007-hover)}
[data-vibeui-block="menubar-007"] [data-part="item"]:focus-visible{outline:2px solid var(--vibeui-menubar-007-accent);outline-offset:-2px}
[data-vibeui-block="menubar-007"] [data-part="keys"]{font-size:0.6875rem;color:var(--vibeui-menubar-007-muted)}
/* Стрелка-«галочка» из бордюров: пункт с продолжением видно без иконочного шрифта. */
[data-vibeui-block="menubar-007"] [data-part="more"]{
width:0.375rem;height:0.375rem;border:1.5px solid var(--vibeui-menubar-007-muted);
border-left:0;border-bottom:0;transform:rotate(45deg);
}
/* Подменю открывает наведение или фокус внутри строки: состояния и JS не нужно. */
[data-vibeui-block="menubar-007"] [data-part="sub"]{
display:none;position:absolute;top:-0.25rem;left:100%;z-index:40;
min-width:11rem;padding:0.25rem;box-sizing:border-box;margin:0 0 0 0.25rem;list-style:none;
background:var(--vibeui-menubar-007-bg);
border:1px solid var(--vibeui-menubar-007-border);border-radius:0.625rem;
box-shadow:0 16px 36px -18px oklch(0.2 0.03 265 / 45%);
}
[data-vibeui-block="menubar-007"] [data-part="row"]:hover > [data-part="sub"],
[data-vibeui-block="menubar-007"] [data-part="row"]:focus-within > [data-part="sub"]{display:block}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="menubar-007"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_MENUS: Menubar007Menu[] = [
  {
    label: "Файл",
    items: [
      { label: "Новый проект", keys: "⌘N" },
      {
        label: "Открыть недавние",
        items: [
          { label: "Лендинг студии" },
          { label: "Документация" },
          { label: "Магазин пряжи" },
        ],
      },
      {
        label: "Экспорт",
        items: [
          { label: "В PDF" },
          { label: "В PNG", keys: "⌘E" },
          { label: "В архив" },
        ],
      },
      { label: "Сохранить", keys: "⌘S" },
    ],
  },
  {
    label: "Правка",
    items: [
      { label: "Отменить", keys: "⌘Z" },
      {
        label: "Преобразовать",
        items: [{ label: "В заглавные" }, { label: "В строчные" }],
      },
    ],
  },
]

/**
 * Строка меню с вложенными подменю, раскрывающимися вбок без JS.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Menubar007({
  menus = DEFAULT_MENUS,
  accent,
  className,
  style,
}: Menubar007Props) {
  const palette = {
    ...(accent ? { "--vibeui-menubar-007-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-menubar-007" precedence="medium">
        {STYLES}
      </style>
      <div
        data-vibeui-block="menubar-007"
        role="menubar"
        aria-label="Меню приложения"
        className={className}
        style={palette}
      >
        {menus.map((menu) => (
          <details key={menu.label} data-part="slot" name="vibeui-menubar-007">
            <summary data-part="trigger" role="menuitem">
              {menu.label}
            </summary>
            <ul data-part="menu" role="menu" aria-label={menu.label}>
              {menu.items.map((item) => (
                <li key={item.label} data-part="row" role="none">
                  <button
                    type="button"
                    data-part="item"
                    role="menuitem"
                    aria-haspopup={item.items ? "menu" : undefined}
                    aria-expanded={item.items ? false : undefined}
                  >
                    {item.label}
                    {item.items ? (
                      <span data-part="more" aria-hidden="true" />
                    ) : item.keys ? (
                      <span data-part="keys">{item.keys}</span>
                    ) : null}
                  </button>
                  {item.items ? (
                    <ul data-part="sub" role="menu" aria-label={item.label}>
                      {item.items.map((child) => (
                        <li key={child.label} role="none">
                          <button
                            type="button"
                            data-part="item"
                            role="menuitem"
                          >
                            {child.label}
                            {child.keys ? (
                              <span data-part="keys">{child.keys}</span>
                            ) : null}
                          </button>
                        </li>
                      ))}
                    </ul>
                  ) : null}
                </li>
              ))}
            </ul>
          </details>
        ))}
      </div>
    </>
  )
}