Navigation

Shortcut Menubar

A menubar where the shortcuts line up in a column of their own and are written platform-independently: ⌘ on Mac, Ctrl on Windows.

  • menubar
  • shortcuts
  • no-js
  • kbd

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-003?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-003" (Shortcut 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-003.json

Registry item: https://vibeui.ru/r/menubar-003.json
Installs to: components/vibeui/menubar-003.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 where the shortcuts line up in a column of their own and are written platform-independently: ⌘ on Mac, Ctrl on Windows.

A menubar with a shortcut column: labels on the left, key combinations on the right, every key in its own <kbd>. Sections close one another without JS — that is the name attribute on <details>. Shortcuts are stored as "Mod S" and expanded for the platform. Zero dependencies, one file, no client JS.

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

<Menubar003
  platform="windows"
  menus={[{ label: "File", items: [{ label: "Save", keys: "Mod S" }] }]}
/>

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-003-* palette — do not swap it for your theme tokens (bg-muted, border-border and the like)
- the shared name attribute on <details>: it is what closes the neighbouring section, and without it every section opens at once
- the two-column grid inside an item: the shortcut has to sit in a shared column or the eye cannot scan it downwards
- writing shortcuts with Mod instead of a hardcoded ⌘: on Windows the ⌘ symbol means nothing
- each key in its own <kbd> — that is key semantics, not decoration
- disabled instead of hiding an unavailable item: vanishing items break the memory of a menu

## 6. You may change
- the menus array and the shortcut strings in the keys field
- platform — which platform Mod expands for
- the accent through the accent prop — it colours the focus ring
- the <kbd> styling: border, background and size

## 7. Rules
- The component does not bind the shortcuts: the label is a reminder, the handler is yours.
- Escape does not close a <details>: that is the element's behaviour, not a bug. If you need Escape, take the popover-based variant.
- An open section overlaps whatever is beneath it: the menubar has to sit higher in the stacking order.
- 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-003.json
https://vibeui.ru/r/menubar-003.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-menubar-003-*. Серверный: хуков нет, состояние держит браузер. Каждый раздел — <details> с общим атрибутом name, поэтому браузер сам следит, что раскрыт только один. Пункт — сетка из двух колонок, сочетание всегда прижато к правому краю. Подписи клавиш хранятся как «Mod S» и разворачиваются функцией keysFor в ⌘ или Ctrl по пропу platform, каждая клавиша выводится отдельным <kbd>.

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 Menubar003Item = {
  label: string
  keys?: string
  disabled?: boolean
}

export type Menubar003Menu = {
  label: string
  items: Menubar003Item[]
}

export type Menubar003Props = {
  menus?: Menubar003Menu[]
  platform?: "mac" | "windows"
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: строка меню, где сочетания клавиш выстроены в общую колонку,
// а не приклеены к тексту пункта — по такому списку сочетание находится взглядом
// сверху вниз. Взаимное закрытие разделов держит атрибут name у <details>:
// браузер сам следит, что открыт только один, без единой строки JS.
const STYLES = `
:where([data-vibeui-block="menubar-003"]){
--vibeui-menubar-003-bg:oklch(1 0 0);
--vibeui-menubar-003-fg:oklch(0.24 0.014 265);
--vibeui-menubar-003-muted:oklch(0.58 0.014 265);
--vibeui-menubar-003-border:oklch(0.9 0.006 265);
--vibeui-menubar-003-hover:oklch(0.55 0.02 265 / 10%);
--vibeui-menubar-003-key:oklch(0.96 0.004 265);
--vibeui-menubar-003-accent:oklch(0.55 0.2 262);
--vibeui-menubar-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="menubar-003"]{
box-sizing:border-box;width:100%;max-width:34rem;padding:0.25rem;
display:flex;align-items:center;gap:0.125rem;
background:var(--vibeui-menubar-003-bg);color:var(--vibeui-menubar-003-fg);
border:1px solid var(--vibeui-menubar-003-border);border-radius:0.625rem;
font-family:var(--vibeui-menubar-003-font);
}
[data-vibeui-block="menubar-003"] [data-part="slot"]{position:relative}
[data-vibeui-block="menubar-003"] [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;
transition:background-color .14s ease;
}
[data-vibeui-block="menubar-003"] [data-part="trigger"]::-webkit-details-marker{display:none}
[data-vibeui-block="menubar-003"] [data-part="trigger"]:hover{background:var(--vibeui-menubar-003-hover)}
[data-vibeui-block="menubar-003"] [data-part="trigger"]:focus-visible{outline:2px solid var(--vibeui-menubar-003-accent);outline-offset:-2px}
[data-vibeui-block="menubar-003"] [data-part="slot"][open] [data-part="trigger"]{background:var(--vibeui-menubar-003-hover)}
[data-vibeui-block="menubar-003"] [data-part="menu"]{
position:absolute;top:calc(100% + 0.375rem);left:0;z-index:30;
min-width:14rem;padding:0.25rem;box-sizing:border-box;
background:var(--vibeui-menubar-003-bg);
border:1px solid var(--vibeui-menubar-003-border);border-radius:0.625rem;
box-shadow:0 16px 36px -18px oklch(0.2 0.03 265 / 45%);
}
/* Пункт — сетка из двух колонок: подпись слева, сочетание всегда у правого края. */
[data-vibeui-block="menubar-003"] [data-part="item"]{
display:grid;grid-template-columns:1fr auto;align-items:center;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-003"] [data-part="item"]:hover:not(:disabled){background:var(--vibeui-menubar-003-hover)}
[data-vibeui-block="menubar-003"] [data-part="item"]:focus-visible{outline:2px solid var(--vibeui-menubar-003-accent);outline-offset:-2px}
[data-vibeui-block="menubar-003"] [data-part="item"]:disabled{color:var(--vibeui-menubar-003-muted);cursor:default}
[data-vibeui-block="menubar-003"] kbd{
justify-self:end;font-family:inherit;font-size:0.6875rem;line-height:1;
padding:0.1875rem 0.3125rem;border-radius:0.25rem;
background:var(--vibeui-menubar-003-key);color:var(--vibeui-menubar-003-muted);
border:1px solid var(--vibeui-menubar-003-border);
}
[data-vibeui-block="menubar-003"] kbd + kbd{margin-left:0.1875rem}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="menubar-003"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_MENUS: Menubar003Menu[] = [
  {
    label: "Файл",
    items: [
      { label: "Новый проект", keys: "Mod N" },
      { label: "Открыть…", keys: "Mod O" },
      { label: "Сохранить", keys: "Mod S" },
      { label: "Экспорт", keys: "Mod Shift E" },
    ],
  },
  {
    label: "Правка",
    items: [
      { label: "Отменить", keys: "Mod Z" },
      { label: "Повторить", keys: "Mod Shift Z" },
      { label: "Заменить", keys: "Mod R" },
      { label: "Вставить историю", disabled: true },
    ],
  },
  {
    label: "Вид",
    items: [
      { label: "Палитра команд", keys: "Mod K" },
      { label: "Боковая панель", keys: "Mod B" },
      { label: "Во весь экран", keys: "F11" },
    ],
  },
]

/**
 * Сочетание записывается платформонезависимо: `Mod` подставляется на ⌘ или Ctrl,
 * поэтому один и тот же список подписей работает на обеих платформах.
 */
function keysFor(keys: string, platform: "mac" | "windows") {
  return keys
    .replace("Mod", platform === "mac" ? "⌘" : "Ctrl")
    .replace("Shift", platform === "mac" ? "⇧" : "Shift")
    .split(" ")
}

/**
 * Строка меню с колонкой горячих клавиш и взаимно закрывающимися разделами.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Menubar003({
  menus = DEFAULT_MENUS,
  platform = "mac",
  accent,
  className,
  style,
}: Menubar003Props) {
  const palette = {
    ...(accent ? { "--vibeui-menubar-003-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-menubar-003" precedence="medium">
        {STYLES}
      </style>
      <div
        data-vibeui-block="menubar-003"
        role="menubar"
        aria-label="Меню приложения"
        className={className}
        style={palette}
      >
        {menus.map((menu) => (
          <details key={menu.label} data-part="slot" name="vibeui-menubar-003">
            <summary data-part="trigger" role="menuitem">
              {menu.label}
            </summary>
            <div data-part="menu" role="menu" aria-label={menu.label}>
              {menu.items.map((item) => (
                <button
                  key={item.label}
                  type="button"
                  data-part="item"
                  role="menuitem"
                  disabled={item.disabled}
                >
                  <span>{item.label}</span>
                  {item.keys ? (
                    <span>
                      {keysFor(item.keys, platform).map((part) => (
                        <kbd key={part}>{part}</kbd>
                      ))}
                    </span>
                  ) : null}
                </button>
              ))}
            </div>
          </details>
        ))}
      </div>
    </>
  )
}