Navigation
Grouped Nav
Меню с разделами и подпунктами без единой строки JS: группы собраны на <details>, а группа с текущим пунктом открыта заранее.
- sidebar
- groups
- details
- navigation
Превью
Использовать с AI
- 1. Скопируйте ссылку.
- 2. Напишите агенту своими словами и вставьте её в предложение.
- 3. Агент откроет ссылку и поставит компонент из registry.
размести это в шапке: https://vibeui.ru/c/sidebar-004
Показать полную инструкцию
То, что лежит по ссылке в развёрнутом виде. Нужна, если агент не может открыть ссылку — тогда вставьте этот текст целиком.
# Установка и размещение «sidebar-004» (Grouped Nav) из VibeUI
## 1. Сначала установка — не пропускай, не пересоздавай
Выполни эту команду до того, как писать код:
npx shadcn@latest add https://vibeui.ru/r/sidebar-004.json
Registry item: https://vibeui.ru/r/sidebar-004.json
Ставится в: components/vibeui/sidebar-004.tsx (точный путь следует алиасам components.json этого проекта).
npm-зависимости: нет.
Registry-зависимости: нет.
Ставь из реестра. Не пересоздавай по описанию, не подменяй похожим
компонентом из другой библиотеки и не переписывай под стиль проекта.
## 2. Что это
Меню с разделами и подпунктами без единой строки JS: группы собраны на <details>, а группа с текущим пунктом открыта заранее.
Раскрывающиеся разделы меню на <details>: текущая группа открыта, свёрнутая с активным пунктом помечена точкой. Ноль зависимостей, один файл, клиентского JS нет.
## 3. Как использовать
import { Sidebar004 } from "@/components/vibeui/sidebar-004"
<Sidebar004
activeLabel="Возвраты"
groups={[
{ label: "Каталог", children: [{ label: "Товары", href: "/goods" }] },
{ label: "Продажи", children: [{ label: "Возвраты", href: "/returns" }] },
]}
/>
Полный список пропсов — в установленном файле.
## 4. Куда поставить
Это мелкий inline-компонент. Поставь его ровно туда, куда просил
пользователь, внутрь существующей разметки. Не создавай ради него
новую страницу, секцию или обёртку. Если на этом месте уже стоит похожий
контрол — замени его, а не добавляй второй.
Куда поставить: ___
(Эту строку заполняет пользователь. Если она осталась пустой — спроси,
а не угадывай.)
## 5. Сохранить как установлено
- локальную палитру --vibeui-sidebar-004-* и не подменять её токенами вашей темы
- <details> вместо самодельного раскрытия: клавиатура и поиск по странице работают сами
- открытую группу с текущим пунктом: меню не должно требовать клика, чтобы показать место
- точку у свёрнутой группы с активным потомком — иначе «где я» теряется при закрытии
- aria-current="page" на активной ссылке, а не только заливку
- видимую обводку фокуса на summary и ссылках
## 6. Можно менять
- разделы и подпункты через groups и вложенный children
- текущий пункт через activeLabel
- цвет акцента через accent
- ширину колонки через max-width корневого элемента
## 7. Правила
- Раскрытие не запоминается между переходами: это цена отказа от клиентского состояния.
- Открытыми могут быть сразу несколько групп — аккордеона здесь намеренно нет.
- Активный пункт ищется по тексту подписи: одинаковые подписи в разных группах подсветятся обе.
- Не выносите CSS-переменные в globals.css: компонент обязан оставаться одним файлом.
## 8. Проверка
- рендерится без ошибок в консоли;
- выглядит так же, как превью на странице VibeUI, откуда взята ссылка;
- если нет — ты изменил что-то из раздела 5, верни как было.Для разработчика
npx shadcn@latest add https://vibeui.ru/r/sidebar-004.jsonhttps://vibeui.ru/r/sidebar-004.jsonКомпонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-sidebar-004-*. Серверный: хуков нет, состояние раскрытия держит браузер через <details>. Атрибут open вычисляется на рендере из того, лежит ли активный пункт внутри группы: меню не должно требовать клика, чтобы показать, где пользователь находится. Свёрнутая группа с активным потомком помечена точкой в summary — после закрытия раздела текущее место иначе пропадает из виду. Галка нарисована бордюрами псевдоэлемента, маркер details скрыт.
Исходник компонента
Тот же файл, который поставит агент. Нужен, если вы предпочитаете скопировать код руками.
import type { ComponentPropsWithoutRef, CSSProperties } from "react"
export type Sidebar004Child = {
label: string
href?: string
}
export type Sidebar004Group = {
label: string
children: Sidebar004Child[]
}
export type Sidebar004Props = Omit<
ComponentPropsWithoutRef<"nav">,
"children"
> & {
groups?: Sidebar004Group[]
activeLabel?: string
accent?: string
}
// Идея компонента: раскрывающиеся разделы без единой строки JS — каждая
// группа это <details>, а состояние держит браузер. Группа с текущим
// пунктом открыта заранее: меню не должно требовать клика, чтобы показать,
// где пользователь находится. Свёрнутая группа с активным потомком помечена
// точкой, иначе после закрытия раздела текущее место пропадает из виду.
const STYLES = `
:where([data-vibeui-block="sidebar-004"]){
--vibeui-sidebar-004-bg:oklch(1 0 0);
--vibeui-sidebar-004-fg:oklch(0.25 0.016 265);
--vibeui-sidebar-004-muted:oklch(0.55 0.014 265);
--vibeui-sidebar-004-border:oklch(0.91 0.006 265);
--vibeui-sidebar-004-accent:oklch(0.54 0.16 300);
--vibeui-sidebar-004-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="sidebar-004"]{
display:flex;flex-direction:column;gap:0.125rem;
width:100%;max-width:15rem;box-sizing:border-box;padding:0.625rem;
background:var(--vibeui-sidebar-004-bg);color:var(--vibeui-sidebar-004-fg);
border:1px solid var(--vibeui-sidebar-004-border);border-radius:0.875rem;
font-family:var(--vibeui-sidebar-004-font);
}
[data-vibeui-block="sidebar-004"] summary{
display:flex;align-items:center;gap:0.5rem;cursor:pointer;list-style:none;
padding:0.4375rem 0.5rem;border-radius:0.5rem;
font-size:0.875rem;font-weight:600;line-height:1.3;
}
[data-vibeui-block="sidebar-004"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="sidebar-004"] summary:hover{background:oklch(0.55 0.02 265 / 7%)}
[data-vibeui-block="sidebar-004"] summary:focus-visible{outline:2px solid var(--vibeui-sidebar-004-accent);outline-offset:-2px}
/* Галка нарисована бордюрами: иконочный шрифт в самодостаточный файл не влезет. */
[data-vibeui-block="sidebar-004"] [data-part="chevron"]{
width:0.375rem;height:0.375rem;flex:none;
border-right:1.5px solid var(--vibeui-sidebar-004-muted);
border-bottom:1.5px solid var(--vibeui-sidebar-004-muted);
transform:rotate(-45deg);transition:transform .16s ease;
}
[data-vibeui-block="sidebar-004"] details[open] > summary [data-part="chevron"]{transform:rotate(45deg)}
/* Точка у свёрнутой группы: закрыв раздел, не теряем «где я». */
[data-vibeui-block="sidebar-004"] [data-part="mark"]{
margin-left:auto;width:0.375rem;height:0.375rem;border-radius:9999px;
background:var(--vibeui-sidebar-004-accent);
}
[data-vibeui-block="sidebar-004"] details[open] > summary [data-part="mark"]{display:none}
[data-vibeui-block="sidebar-004"] ul{
margin:0.0625rem 0 0.375rem 0.6875rem;padding:0 0 0 0.5rem;list-style:none;
display:flex;flex-direction:column;gap:0.0625rem;
border-left:1px solid var(--vibeui-sidebar-004-border);
}
[data-vibeui-block="sidebar-004"] a{
display:block;padding:0.3125rem 0.5rem;border-radius:0.375rem;
color:var(--vibeui-sidebar-004-muted);text-decoration:none;
font-size:0.8125rem;line-height:1.3;
}
[data-vibeui-block="sidebar-004"] a:hover{background:oklch(0.55 0.02 265 / 7%);color:var(--vibeui-sidebar-004-fg)}
[data-vibeui-block="sidebar-004"] a:focus-visible{outline:2px solid var(--vibeui-sidebar-004-accent);outline-offset:-2px}
[data-vibeui-block="sidebar-004"] a[aria-current="page"]{
color:var(--vibeui-sidebar-004-fg);font-weight:600;
background:color-mix(in oklab,var(--vibeui-sidebar-004-accent) 12%,transparent);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="sidebar-004"] *{animation:none!important;transition:none!important}}
`
const DEFAULT_GROUPS: Sidebar004Group[] = [
{
label: "Каталог",
children: [
{ label: "Товары", href: "#" },
{ label: "Категории", href: "#" },
{ label: "Бренды", href: "#" },
],
},
{
label: "Продажи",
children: [
{ label: "Заказы", href: "#" },
{ label: "Возвраты", href: "#" },
{ label: "Промокоды", href: "#" },
],
},
{
label: "Настройки",
children: [
{ label: "Доставка", href: "#" },
{ label: "Оплата", href: "#" },
],
},
]
/**
* Меню с разделами и подпунктами: группы на details, текущая раскрыта заранее.
* Один файл, ноль зависимостей, собственная палитра.
*/
export function Sidebar004({
groups = DEFAULT_GROUPS,
activeLabel = "Возвраты",
accent,
className,
style,
...props
}: Sidebar004Props) {
const palette = {
...(accent ? { "--vibeui-sidebar-004-accent": accent } : null),
...style,
} as CSSProperties
return (
<>
<style href="vibeui-sidebar-004" precedence="medium">
{STYLES}
</style>
<nav
{...props}
data-vibeui-block="sidebar-004"
aria-label="Разделы магазина"
className={className}
style={palette}
>
{groups.map((group) => {
const holdsActive = group.children.some(
(child) => child.label === activeLabel,
)
return (
<details key={group.label} open={holdsActive}>
<summary>
<span data-part="chevron" aria-hidden="true" />
{group.label}
{holdsActive ? (
<span data-part="mark" aria-hidden="true" />
) : null}
</summary>
<ul>
{group.children.map((child) => (
<li key={child.label}>
<a
href={child.href}
aria-current={
child.label === activeLabel ? "page" : undefined
}
>
{child.label}
</a>
</li>
))}
</ul>
</details>
)
})}
</nav>
</>
)
}