Navigation

Simple Navmenu

Самое простое выпадающее меню разделов: столбик ссылок без описаний и картинок, взаимное закрытие держит атрибут name у details.

  • navmenu
  • dropdown
  • details
  • no-js

Превью

1440pxHost theme

Использовать с AI

  1. 1. Скопируйте ссылку.
  2. 2. Напишите агенту своими словами и вставьте её в предложение.
  3. 3. Агент откроет ссылку и поставит компонент из registry.

размести это в шапке: https://vibeui.ru/c/navmenu-003

Показать полную инструкцию

То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.

# Установка и размещение «navmenu-003» (Simple Navmenu) из VibeUI

## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/navmenu-003.json

Registry item: https://vibeui.ru/r/navmenu-003.json
Ставится в: components/vibeui/navmenu-003.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.

Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.

## 2. Что это
Самое простое выпадающее меню разделов: столбик ссылок без описаний и картинок, взаимное закрытие держит атрибут name у details.

Обычное меню сайта: разделы с выпадающим столбиком ссылок и разделы-ссылки без списка. Открытие и взаимное закрытие держат <details> с общим name, поэтому меню работает до гидратации и при отключённых скриптах. Ноль зависимостей, один файл, клиентского JS нет.

## 3. Как использовать
import { Navmenu003 } from "@/components/vibeui/navmenu-003"

<Navmenu003
  groups={[
    { label: "Продукт", items: ["Редактор", "Шаблоны"] },
    { label: "Цены", href: "/pricing" },
  ]}
/>

Полный список пропсов — в установленном файле.

## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.

Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)

## 5. Сохранить как установлено
- локальную палитру --vibeui-navmenu-003-* и не подменять её токенами вашей темы (bg-popover, border-border и подобными)
- общий атрибут name у <details>: без него открываются сразу все разделы
- переворот галочки по атрибуту open — состояние кнопки должно читаться на ней самой
- разделы без подпунктов обычными ссылками: кнопка вместо ссылки лишает открытия в новой вкладке
- разметку меню списком <ul>: набор ссылок — это список, а не стопка div'ов
- видимое кольцо фокуса на кнопках и ссылках: меню обязано проходиться табуляцией

## 6. Можно менять
- массив groups: разделы, их пункты и адреса
- ширину выпадающего столбика
- акцент через проп accent — цвет колец фокуса
- высоту полосы навигации

## 7. Правила
- Escape не закрывает <details>: это поведение элемента. Нужна реакция на Escape — берите вариант на popover.
- Клик мимо тоже не закрывает раздел: за это в <details> никто не отвечает.
- Выпадающий столбик перекрывает содержимое под собой: полоса навигации должна лежать выше по слою.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.

Для разработчика

npx shadcn@latest add https://vibeui.ru/r/navmenu-003.json
https://vibeui.ru/r/navmenu-003.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-navmenu-003-*. Серверный: хуков нет. Раздел с подпунктами — <details> с общим атрибутом name, поэтому браузер сам следит, что раскрыт только один. Меню позиционируется абсолютно от своей обёртки, anchor positioning не требуется. Галочка на кнопке переворачивается по атрибуту open, поэтому состояние видно на самой кнопке. Разделы без подпунктов — обычные ссылки.

Исходник компонента

Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.

import type { CSSProperties } from "react"

export type Navmenu003Group = {
  label: string
  items?: string[]
  href?: string
}

export type Navmenu003Props = {
  groups?: Navmenu003Group[]
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: самое простое выпадающее меню разделов — один столбик ссылок
// без описаний, картинок и колонок. Открытие и взаимное закрытие держит атрибут
// name у <details>: браузер сам следит, что раскрыт только один раздел. JS нет,
// значит меню работает и до гидратации, и при отключённых скриптах.
const STYLES = `
:where([data-vibeui-block="navmenu-003"]){
--vibeui-navmenu-003-bg:oklch(1 0 0);
--vibeui-navmenu-003-fg:oklch(0.22 0.014 265);
--vibeui-navmenu-003-muted:oklch(0.55 0.014 265);
--vibeui-navmenu-003-border:oklch(0.91 0.006 265);
--vibeui-navmenu-003-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-navmenu-003-accent:oklch(0.55 0.2 262);
--vibeui-navmenu-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="navmenu-003"]{
box-sizing:border-box;width:100%;max-width:32rem;padding:0.375rem;
display:flex;align-items:center;gap:0.125rem;
background:var(--vibeui-navmenu-003-bg);color:var(--vibeui-navmenu-003-fg);
border:1px solid var(--vibeui-navmenu-003-border);border-radius:0.75rem;
font-family:var(--vibeui-navmenu-003-font);
}
[data-vibeui-block="navmenu-003"] [data-part="slot"]{position:relative}
[data-vibeui-block="navmenu-003"] [data-part="trigger"],
[data-vibeui-block="navmenu-003"] [data-part="plain"]{
list-style:none;cursor:pointer;text-decoration:none;color:inherit;
display:inline-flex;align-items:center;gap:0.375rem;
height:2rem;padding:0 0.75rem;border-radius:0.5rem;font-size:0.875rem;
}
[data-vibeui-block="navmenu-003"] [data-part="trigger"]::-webkit-details-marker{display:none}
[data-vibeui-block="navmenu-003"] [data-part="trigger"]:hover,
[data-vibeui-block="navmenu-003"] [data-part="plain"]:hover{background:var(--vibeui-navmenu-003-hover)}
[data-vibeui-block="navmenu-003"] [data-part="trigger"]:focus-visible,
[data-vibeui-block="navmenu-003"] [data-part="plain"]:focus-visible{outline:2px solid var(--vibeui-navmenu-003-accent);outline-offset:-2px}
[data-vibeui-block="navmenu-003"] [data-part="slot"][open] > [data-part="trigger"]{background:var(--vibeui-navmenu-003-hover)}
/* Галочка переворачивается вместе с раскрытием: состояние видно на самой кнопке. */
[data-vibeui-block="navmenu-003"] [data-part="caret"]{
width:0.375rem;height:0.375rem;margin-top:-0.1875rem;
border:1.5px solid var(--vibeui-navmenu-003-muted);border-left:0;border-top:0;
transform:rotate(45deg);transition:transform .16s ease;
}
[data-vibeui-block="navmenu-003"] [data-part="slot"][open] [data-part="caret"]{transform:rotate(-135deg);margin-top:0.125rem}
[data-vibeui-block="navmenu-003"] [data-part="menu"]{
position:absolute;top:calc(100% + 0.5rem);left:0;z-index:30;
min-width:12rem;margin:0;padding:0.25rem;box-sizing:border-box;list-style:none;
background:var(--vibeui-navmenu-003-bg);
border:1px solid var(--vibeui-navmenu-003-border);border-radius:0.75rem;
box-shadow:0 20px 40px -22px oklch(0.2 0.03 265 / 42%);
}
[data-vibeui-block="navmenu-003"] [data-part="link"]{
display:block;padding:0.4375rem 0.5rem;border-radius:0.5rem;
text-decoration:none;color:inherit;font-size:0.875rem;
}
[data-vibeui-block="navmenu-003"] [data-part="link"]:hover{background:var(--vibeui-navmenu-003-hover)}
[data-vibeui-block="navmenu-003"] [data-part="link"]:focus-visible{outline:2px solid var(--vibeui-navmenu-003-accent);outline-offset:-2px}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="navmenu-003"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_GROUPS: Navmenu003Group[] = [
  {
    label: "Продукт",
    items: ["Редактор", "Шаблоны", "Аналитика", "Интеграции"],
  },
  {
    label: "Компания",
    items: ["О нас", "Вакансии", "Контакты"],
  },
  { label: "Цены", href: "#" },
  { label: "Блог", href: "#" },
]

/**
 * Простое выпадающее меню разделов: один столбик ссылок, без JS.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Navmenu003({
  groups = DEFAULT_GROUPS,
  accent,
  className,
  style,
}: Navmenu003Props) {
  const palette = {
    ...(accent ? { "--vibeui-navmenu-003-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-navmenu-003" precedence="medium">
        {STYLES}
      </style>
      <nav
        data-vibeui-block="navmenu-003"
        aria-label="Основная навигация"
        className={className}
        style={palette}
      >
        {groups.map((group) =>
          group.items ? (
            <details
              key={group.label}
              data-part="slot"
              name="vibeui-navmenu-003"
            >
              <summary data-part="trigger">
                {group.label}
                <span data-part="caret" aria-hidden="true" />
              </summary>
              <ul data-part="menu" aria-label={group.label}>
                {group.items.map((item) => (
                  <li key={item}>
                    <a data-part="link" href="#">
                      {item}
                    </a>
                  </li>
                ))}
              </ul>
            </details>
          ) : (
            <a key={group.label} data-part="plain" href={group.href ?? "#"}>
              {group.label}
            </a>
          ),
        )}
      </nav>
    </>
  )
}