Navigation

Column Navmenu

Навигация сайта с выпадающей панелью из колонок: под каждой ссылкой строка пояснения, поэтому по разделу понятно, что внутри.

  • navmenu
  • mega menu
  • popover
  • site nav

Превью

1440pxHost theme

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

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

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

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

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

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

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

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

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

## 2. Что это
Навигация сайта с выпадающей панелью из колонок: под каждой ссылкой строка пояснения, поэтому по разделу понятно, что внутри.

Навигация сайта с широкой выпадающей панелью: колонки ссылок, у каждой ссылки строка пояснения. Панель открывает HTML popover и якорит к полосе навигации, поэтому Escape и клик мимо достаются от браузера. Ноль зависимостей, один файл, клиентского JS нет.

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

<Navmenu001
  entries={[
    {
      label: "Продукт",
      columns: [
        { title: "Инструменты", links: [{ label: "Редактор", hint: "Блоки и сетка" }] },
      ],
    },
    { label: "Цены", href: "/pricing" },
  ]}
/>

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

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

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

## 5. Сохранить как установлено
- локальную палитру --vibeui-navmenu-001-* и не подменять её токенами вашей темы (bg-popover, text-muted-foreground и подобными)
- пояснение под каждой ссылкой: имя раздела в мега-меню почти никогда не объясняет, что там внутри
- якорь на всей полосе, а не на отдельной кнопке: широкая панель, привязанная к кнопке, съезжает за край окна
- popover="auto": Escape, клик мимо и закрытие соседней панели достаются от браузера
- auto-fit с minmax в колонках — при узкой панели они перестраиваются сами, без второго набора правил
- разделы без панели обычными ссылками: кнопка, которая просто ведёт на страницу, ломает открытие в новой вкладке

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

## 7. Правила
- Панель не открывается по наведению — только по клику: случайное раскрытие мега-меню при проезде курсором раздражает сильнее, чем лишний клик.
- Без поддержки anchor positioning панель открывается по центру экрана: проверьте целевые браузеры.
- Больше трёх колонок в панели не ставьте: список перестаёт читаться и превращается в стену ссылок.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.

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

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

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

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-navmenu-001-*. Серверный: хуков нет, панель открывает popover="auto". Якорем служит вся полоса навигации (anchor-name на [data-part="bar"]), поэтому широкая панель встаёт под полосой целиком, а не под отдельной кнопкой. Колонки раскладывает grid с auto-fit и minmax, так что их число подстраивается под ширину панели. Разделы без панели — обычные ссылки.

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

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

import type { CSSProperties } from "react"

export type Navmenu001Link = {
  label: string
  hint?: string
  href?: string
}

export type Navmenu001Column = {
  title: string
  links: Navmenu001Link[]
}

export type Navmenu001Entry = {
  label: string
  href?: string
  columns?: Navmenu001Column[]
}

export type Navmenu001Props = {
  entries?: Navmenu001Entry[]
  accent?: string
  className?: string
  style?: CSSProperties
}

// Идея компонента: навигация сайта, где выпадающая панель — не список ссылок, а
// колонки с пояснениями: по подписи «Аналитика» непонятно, что внутри, по строке
// под ней — понятно. Панель открывает HTML popover, а якорем служит вся полоса
// навигации, поэтому панель встаёт под ней целиком, а не под отдельной кнопкой.
const STYLES = `
:where([data-vibeui-block="navmenu-001"]){
--vibeui-navmenu-001-bg:oklch(1 0 0);
--vibeui-navmenu-001-fg:oklch(0.22 0.014 265);
--vibeui-navmenu-001-muted:oklch(0.55 0.014 265);
--vibeui-navmenu-001-border:oklch(0.91 0.006 265);
--vibeui-navmenu-001-hover:oklch(0.55 0.02 265 / 8%);
--vibeui-navmenu-001-accent:oklch(0.55 0.2 262);
--vibeui-navmenu-001-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="navmenu-001"]{
box-sizing:border-box;width:100%;max-width:44rem;
font-family:var(--vibeui-navmenu-001-font);color:var(--vibeui-navmenu-001-fg);
}
[data-vibeui-block="navmenu-001"] [data-part="bar"]{
box-sizing:border-box;padding:0.375rem;
display:flex;align-items:center;gap:0.125rem;
background:var(--vibeui-navmenu-001-bg);
border:1px solid var(--vibeui-navmenu-001-border);border-radius:0.75rem;
anchor-name:--vibeui-navmenu-001-bar;
}
[data-vibeui-block="navmenu-001"] [data-part="trigger"],
[data-vibeui-block="navmenu-001"] [data-part="plain"]{
appearance:none;border:0;background:none;cursor:pointer;text-decoration:none;
display:inline-flex;align-items:center;gap:0.375rem;
height:2rem;padding:0 0.75rem;border-radius:0.5rem;
font:inherit;font-size:0.875rem;color:inherit;
transition:background-color .14s ease;
}
[data-vibeui-block="navmenu-001"] [data-part="trigger"]:hover,
[data-vibeui-block="navmenu-001"] [data-part="plain"]:hover{background:var(--vibeui-navmenu-001-hover)}
[data-vibeui-block="navmenu-001"] [data-part="trigger"]:focus-visible,
[data-vibeui-block="navmenu-001"] [data-part="plain"]:focus-visible{outline:2px solid var(--vibeui-navmenu-001-accent);outline-offset:-2px}
[data-vibeui-block="navmenu-001"] [data-part="caret"]{
width:0.375rem;height:0.375rem;margin-top:-0.1875rem;
border:1.5px solid var(--vibeui-navmenu-001-muted);border-left:0;border-top:0;
transform:rotate(45deg);transition:transform .16s ease;
}
[data-vibeui-block="navmenu-001"] [data-part="panel"]{
position:fixed;inset:auto;margin:0;
width:min(40rem,92vw);padding:1rem;box-sizing:border-box;
background:var(--vibeui-navmenu-001-bg);color:var(--vibeui-navmenu-001-fg);
border:1px solid var(--vibeui-navmenu-001-border);border-radius:0.875rem;
font-family:var(--vibeui-navmenu-001-font);
box-shadow:0 24px 48px -24px oklch(0.2 0.03 265 / 40%);
}
/* Якорь — вся полоса: панель широкая и не должна прыгать за отдельной кнопкой. */
@supports (anchor-name: --a){
[data-vibeui-block="navmenu-001"] [data-part="panel"]{
position-anchor:--vibeui-navmenu-001-bar;
position-area:bottom span-right;margin-top:0.5rem;
position-try-fallbacks:flip-block;
}
}
[data-vibeui-block="navmenu-001"] [data-part="columns"]{
display:grid;grid-template-columns:repeat(auto-fit,minmax(11rem,1fr));gap:1rem 1.5rem;
}
[data-vibeui-block="navmenu-001"] [data-part="title"]{
margin:0 0 0.5rem;font-size:0.6875rem;letter-spacing:0.06em;text-transform:uppercase;
color:var(--vibeui-navmenu-001-muted);
}
[data-vibeui-block="navmenu-001"] [data-part="list"]{margin:0;padding:0;list-style:none;display:grid;gap:0.125rem}
[data-vibeui-block="navmenu-001"] [data-part="link"]{
display:block;padding:0.4375rem 0.5rem;border-radius:0.5rem;text-decoration:none;color:inherit;
}
[data-vibeui-block="navmenu-001"] [data-part="link"]:hover{background:var(--vibeui-navmenu-001-hover)}
[data-vibeui-block="navmenu-001"] [data-part="link"]:focus-visible{outline:2px solid var(--vibeui-navmenu-001-accent);outline-offset:-2px}
[data-vibeui-block="navmenu-001"] [data-part="name"]{display:block;font-size:0.875rem;font-weight:550}
[data-vibeui-block="navmenu-001"] [data-part="hint"]{display:block;margin-top:0.125rem;font-size:0.75rem;line-height:1.45;color:var(--vibeui-navmenu-001-muted)}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="navmenu-001"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ENTRIES: Navmenu001Entry[] = [
  {
    label: "Продукт",
    columns: [
      {
        title: "Инструменты",
        links: [
          { label: "Редактор", hint: "Собирайте страницы из готовых блоков" },
          { label: "Аналитика", hint: "Просмотры, источники и воронки" },
          { label: "Формы", hint: "Заявки падают в почту и в CRM" },
        ],
      },
      {
        title: "Платформа",
        links: [
          { label: "Домены", hint: "Свой адрес и сертификат за минуту" },
          { label: "Хранилище", hint: "Картинки и файлы рядом с сайтом" },
          { label: "Роли", hint: "Кто редактирует, а кто только смотрит" },
        ],
      },
    ],
  },
  {
    label: "Решения",
    columns: [
      {
        title: "Для кого",
        links: [
          { label: "Студиям", hint: "Десятки сайтов в одной панели" },
          { label: "Магазинам", hint: "Каталог, оплата и доставка" },
          { label: "Медиа", hint: "Поток материалов и авторы" },
        ],
      },
    ],
  },
  { label: "Цены", href: "#" },
  { label: "Документация", href: "#" },
]

/**
 * Навигация сайта с выпадающей панелью из колонок ссылок с описаниями.
 * Один файл, ноль зависимостей, собственная палитра, клиентского JS нет.
 */
export function Navmenu001({
  entries = DEFAULT_ENTRIES,
  accent,
  className,
  style,
}: Navmenu001Props) {
  const palette = {
    ...(accent ? { "--vibeui-navmenu-001-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-navmenu-001" precedence="medium">
        {STYLES}
      </style>
      <nav
        data-vibeui-block="navmenu-001"
        aria-label="Основная навигация"
        className={className}
        style={palette}
      >
        <div data-part="bar">
          {entries.map((entry, index) => {
            if (!entry.columns) {
              return (
                <a key={entry.label} data-part="plain" href={entry.href ?? "#"}>
                  {entry.label}
                </a>
              )
            }

            const id = `vibeui-navmenu-001-${index}`

            return (
              <span key={entry.label}>
                <button
                  type="button"
                  data-part="trigger"
                  aria-haspopup="true"
                  popoverTarget={id}
                >
                  {entry.label}
                  <span data-part="caret" aria-hidden="true" />
                </button>
                <div
                  id={id}
                  data-part="panel"
                  popover="auto"
                  aria-label={entry.label}
                >
                  <div data-part="columns">
                    {entry.columns.map((column) => (
                      <div key={column.title}>
                        <p data-part="title">{column.title}</p>
                        <ul data-part="list">
                          {column.links.map((link) => (
                            <li key={link.label}>
                              <a data-part="link" href={link.href ?? "#"}>
                                <span data-part="name">{link.label}</span>
                                {link.hint ? (
                                  <span data-part="hint">{link.hint}</span>
                                ) : null}
                              </a>
                            </li>
                          ))}
                        </ul>
                      </div>
                    ))}
                  </div>
                </div>
              </span>
            )
          })}
        </div>
      </nav>
    </>
  )
}