Navigation

Icon Rail Sidebar

A menu that collapses into an icon rail instead of disappearing: the labels shrink to zero width but stay in the markup for screen readers and in-page search.

  • sidebar
  • collapse
  • icons
  • navigation

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/sidebar-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 "sidebar-003" (Icon Rail Sidebar) 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/sidebar-003.json

Registry item: https://vibeui.ru/r/sidebar-003.json
Installs to: components/vibeui/sidebar-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 menu that collapses into an icon rail instead of disappearing: the labels shrink to zero width but stay in the markup for screen readers and in-page search.

A sidebar with an icon-rail toggle: one variable changes the width and the labels stay in the markup. Zero dependencies, one file.

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

<Sidebar003
  activeLabel="Tasks"
  items={[
    { label: "Overview", href: "/", icon: "M2 8.5 8 3l6 5.5M4 8v5h8V8" },
    { label: "Tasks", href: "/tasks", icon: "M3 4h10M3 8h10M3 12h6" },
  ]}
/>

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-sidebar-003-* palette — do not swap it for your theme tokens
- the labels staying in the markup while collapsed: a removed name leaves the link without an accessible name
- the column width in a single variable — otherwise the state is duplicated in every rule
- the title attribute on collapsed links: an icon alone is not recognised by everyone
- aria-expanded on the toggle, otherwise the menu state is unknown to a screen reader
- the visible focus ring on the links: in a narrow icon rail it is the only landmark

## 6. You may change
- the items through items: label, href and the icon path
- the current item through activeLabel
- the toggle caption through collapsedLabel
- the accent colour through accent and the column width in --vibeui-sidebar-003-width

## 7. Rules
- The icon is a path in a 16×16 coordinate system: a foreign viewBox arrives cropped.
- The active item is matched by label text, not by href: identical labels both light up.
- While collapsed the hint is the native title: it appears with the browser's own delay.
- 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/sidebar-003.json
https://vibeui.ru/r/sidebar-003.json

Компонент самодостаточен: один файл, без зависимостей, палитра в локальных переменных --vibeui-sidebar-003-*. Клиентский ("use client") — состояние сворачивания держит useState. Ширина колонки задана одной переменной --vibeui-sidebar-003-width, которую переопределяет правило на [data-collapsed="true"]: всё остальное подстраивается само. Подписи не удаляются из DOM, а сжимаются max-width и opacity, поэтому доступное имя ссылки не теряется. Иконки — инлайновые пути SVG из props, иконочная библиотека не нужна. В свёрнутом виде у ссылок появляется title, а у кнопки — aria-expanded.

Component source

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

"use client"

import { useState } from "react"
import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Sidebar003Item = {
  label: string
  href?: string
  /** Путь SVG в системе координат 16×16: иконка рисуется без библиотеки. */
  icon: string
}

export type Sidebar003Props = Omit<
  ComponentPropsWithoutRef<"nav">,
  "children"
> & {
  items?: Sidebar003Item[]
  activeLabel?: string
  collapsedLabel?: string
  accent?: string
}

// Идея компонента: меню сворачивается в полосу иконок, а не исчезает. Подписи
// не удаляются из разметки — они схлопываются по ширине, поэтому скринридер
// и поиск по странице продолжают их видеть, а ширина колонки меняется одной
// переменной. В свёрнутом виде у каждой ссылки остаётся title: иконка без
// подписи опознаётся не всеми и не сразу.
const STYLES = `
:where([data-vibeui-block="sidebar-003"]){
--vibeui-sidebar-003-bg:oklch(0.985 0.002 265);
--vibeui-sidebar-003-fg:oklch(0.25 0.016 265);
--vibeui-sidebar-003-muted:oklch(0.55 0.014 265);
--vibeui-sidebar-003-border:oklch(0.91 0.006 265);
--vibeui-sidebar-003-accent:oklch(0.55 0.19 262);
--vibeui-sidebar-003-width:13rem;
--vibeui-sidebar-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="sidebar-003"]{
display:flex;flex-direction:column;gap:0.5rem;
width:100%;max-width:var(--vibeui-sidebar-003-width);box-sizing:border-box;
padding:0.625rem;
background:var(--vibeui-sidebar-003-bg);color:var(--vibeui-sidebar-003-fg);
border:1px solid var(--vibeui-sidebar-003-border);border-radius:0.875rem;
font-family:var(--vibeui-sidebar-003-font);
transition:max-width .2s ease;
}
/* Свёрнутое состояние — одна переменная ширины, остальное подстраивается. */
[data-vibeui-block="sidebar-003"][data-collapsed="true"]{--vibeui-sidebar-003-width:3.5rem}
[data-vibeui-block="sidebar-003"] [data-part="toggle"]{
appearance:none;cursor:pointer;align-self:flex-end;
display:inline-flex;align-items:center;justify-content:center;
width:2rem;height:2rem;padding:0;
border:1px solid var(--vibeui-sidebar-003-border);border-radius:0.5rem;
background:oklch(1 0 0);color:var(--vibeui-sidebar-003-muted);
}
[data-vibeui-block="sidebar-003"] [data-part="toggle"]:hover{color:var(--vibeui-sidebar-003-fg)}
[data-vibeui-block="sidebar-003"] [data-part="toggle"]:focus-visible{outline:2px solid var(--vibeui-sidebar-003-accent);outline-offset:2px}
[data-vibeui-block="sidebar-003"] [data-part="toggle"] svg{width:0.875rem;height:0.875rem;display:block;transition:transform .2s ease}
[data-vibeui-block="sidebar-003"][data-collapsed="true"] [data-part="toggle"]{align-self:center}
[data-vibeui-block="sidebar-003"][data-collapsed="true"] [data-part="toggle"] svg{transform:rotate(180deg)}
[data-vibeui-block="sidebar-003"] ul{margin:0;padding:0;list-style:none;display:flex;flex-direction:column;gap:0.125rem}
[data-vibeui-block="sidebar-003"] a{
display:flex;align-items:center;gap:0.625rem;
padding:0.4375rem 0.5rem;border-radius:0.5rem;
color:var(--vibeui-sidebar-003-muted);text-decoration:none;
font-size:0.875rem;line-height:1.3;white-space:nowrap;
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="sidebar-003"] a:hover{background:oklch(0.55 0.02 265 / 8%);color:var(--vibeui-sidebar-003-fg)}
[data-vibeui-block="sidebar-003"] a:focus-visible{outline:2px solid var(--vibeui-sidebar-003-accent);outline-offset:-2px}
[data-vibeui-block="sidebar-003"] a[aria-current="page"]{
background:color-mix(in oklab,var(--vibeui-sidebar-003-accent) 12%,transparent);
color:var(--vibeui-sidebar-003-fg);font-weight:600;
}
[data-vibeui-block="sidebar-003"] a[aria-current="page"] svg{color:var(--vibeui-sidebar-003-accent)}
[data-vibeui-block="sidebar-003"] svg{width:1rem;height:1rem;flex:none;display:block}
/* Подпись схлопывается по ширине, но остаётся в разметке для скринридера. */
[data-vibeui-block="sidebar-003"] [data-part="label"]{
overflow:hidden;transition:max-width .2s ease,opacity .16s ease;
max-width:8rem;opacity:1;
}
[data-vibeui-block="sidebar-003"][data-collapsed="true"] [data-part="label"]{max-width:0;opacity:0}
[data-vibeui-block="sidebar-003"][data-collapsed="true"] a{justify-content:center;gap:0}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="sidebar-003"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ITEMS: Sidebar003Item[] = [
  { label: "Обзор", href: "#", icon: "M2 8.5 8 3l6 5.5M4 8v5h8V8" },
  { label: "Задачи", href: "#", icon: "M3 4h10M3 8h10M3 12h6" },
  {
    label: "Команда",
    href: "#",
    icon: "M5.5 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm5.5 6H2a3.5 3.5 0 0 1 7 0Zm0 0h4a3 3 0 0 0-4-2.8",
  },
  { label: "Отчёты", href: "#", icon: "M3 13V7m4 6V3m4 10V9" },
  {
    label: "Настройки",
    href: "#",
    icon: "M8 10.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5ZM8 1.5v1.5M8 13v1.5M2.5 8H1M15 8h-1.5",
  },
]

/**
 * Меню, сворачивающееся в полосу иконок: ширина меняется одной переменной.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Sidebar003({
  items = DEFAULT_ITEMS,
  activeLabel = "Задачи",
  collapsedLabel = "Свернуть меню",
  accent,
  className,
  style,
  ...props
}: Sidebar003Props) {
  const [collapsed, setCollapsed] = useState(false)

  const palette = {
    ...(accent ? { "--vibeui-sidebar-003-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-sidebar-003" precedence="medium">
        {STYLES}
      </style>
      <nav
        {...props}
        data-vibeui-block="sidebar-003"
        data-collapsed={collapsed}
        aria-label="Основное меню"
        className={className}
        style={palette}
      >
        <button
          type="button"
          data-part="toggle"
          aria-expanded={!collapsed}
          aria-label={collapsed ? "Развернуть меню" : collapsedLabel}
          onClick={() => setCollapsed((value) => !value)}
        >
          <svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
            <path
              d="M10 3 5 8l5 5"
              strokeWidth="1.6"
              strokeLinecap="round"
              strokeLinejoin="round"
            />
          </svg>
        </button>
        <ul>
          {items.map((item) => (
            <li key={item.label}>
              <a
                href={item.href}
                title={collapsed ? item.label : undefined}
                aria-current={item.label === activeLabel ? "page" : undefined}
              >
                <svg viewBox="0 0 16 16" fill="none" stroke="currentColor">
                  <path
                    d={item.icon}
                    strokeWidth="1.4"
                    strokeLinecap="round"
                    strokeLinejoin="round"
                  />
                </svg>
                <span data-part="label">{item.label}</span>
              </a>
            </li>
          ))}
        </ul>
      </nav>
    </>
  )
}