Menu

Settings Menu

A settings menu with toggles: it stays open on click, because settings are changed in batches.

  • menu
  • settings
  • toggle
  • view

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/menu-005?lang=en

Catalog view

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 "menu-005" (Settings Menu) 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/menu-005.json

Registry item: https://vibeui.ru/r/menu-005.json
Installs to: components/vibeui/menu-005.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 settings menu with toggles: it stays open on click, because settings are changed in batches.

A view and settings menu: a single-choice group, toggles and a separator between them. The menu stays open after a click. Zero dependencies, one file.

## 3. How to use it
import { Menu005 } from "@/components/vibeui/menu-005"

<Menu005 views={["Grid", "List"]} toggles={["Mine only"]} onChange={apply} />

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-menu-005-* palette — do not swap it for your theme tokens
- keeping the menu open: settings are changed in batches, not one at a time
- the permanent slot for the check mark — otherwise rows jump on toggle
- the menuitemradio and menuitemcheckbox roles with aria-checked
- real radios and checkboxes under the labels instead of buttons
- the separator between the view choice and the toggles

## 6. You may change
- the views and toggles arrays
- label — the section heading
- the onChange handler
- the accent through the accent prop

## 7. Rules
- Settings apply immediately: if the action is expensive, add an Apply button.
- This is the menu body without a trigger: wrap it in the popover from menu-001.
- Beyond seven rows, split the menu into labelled sections.
- 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/menu-005.json
https://vibeui.ru/r/menu-005.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в локальных переменных --vibeui-menu-005-*. Клиентский: "use client". Внутри настоящие radio и checkbox со скрытым полем и ролями menuitemradio и menuitemcheckbox. Место под отметку занято всегда, поэтому строки не прыгают при переключении. Разделитель объявлен role="separator".

Component source

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

"use client"

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

export type Menu005Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children" | "onChange"
> & {
  label?: string
  views?: string[]
  toggles?: string[]
  onChange?: (state: { view: string; on: string[] }) => void
  accent?: string
}

// Идея компонента: меню настроек с переключателями и выбором одного из
// вариантов. Состояние остаётся видимым после нажатия, а меню не закрывается:
// настройки почти всегда меняют пачкой, и захлопывающийся список заставляет
// открывать его пять раз подряд.
const STYLES = `
:where([data-vibeui-block="menu-005"]){
--vibeui-menu-005-bg:oklch(1 0 0);
--vibeui-menu-005-fg:oklch(0.24 0.014 265);
--vibeui-menu-005-muted:oklch(0.56 0.014 265);
--vibeui-menu-005-border:oklch(0.9 0.006 265);
--vibeui-menu-005-hover:oklch(0.96 0.004 265);
--vibeui-menu-005-accent:oklch(0.55 0.17 265);
--vibeui-menu-005-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="menu-005"]{
display:flex;flex-direction:column;gap:0.3125rem;
width:100%;max-width:15rem;box-sizing:border-box;padding:0.375rem;
border:1px solid var(--vibeui-menu-005-border);border-radius:0.875rem;
background:var(--vibeui-menu-005-bg);
font-family:var(--vibeui-menu-005-font);color:var(--vibeui-menu-005-fg);
box-shadow:0 18px 40px -26px oklch(0.2 0.02 265 / 45%);
}
[data-vibeui-block="menu-005"] [data-part="label"]{
padding:0.3125rem 0.5rem 0.125rem;
font-size:0.6875rem;font-weight:650;letter-spacing:0.05em;text-transform:uppercase;
color:var(--vibeui-menu-005-muted);
}
[data-vibeui-block="menu-005"] label{
display:flex;align-items:center;gap:0.5rem;
min-height:2rem;padding:0 0.5rem;border-radius:0.5rem;
font-size:0.8125rem;cursor:pointer;
}
[data-vibeui-block="menu-005"] label:hover{background:var(--vibeui-menu-005-hover)}
[data-vibeui-block="menu-005"] input{position:absolute;width:1px;height:1px;opacity:0}
/* Отметка занимает место всегда: без неё строки прыгают при переключении. */
[data-vibeui-block="menu-005"] [data-part="mark"]{
flex:none;width:0.875rem;height:0.875rem;position:relative;
}
[data-vibeui-block="menu-005"] label:has(input:checked) [data-part="mark"]::after{
content:"";position:absolute;left:0.125rem;top:0;
width:0.3125rem;height:0.5625rem;
border-right:2px solid var(--vibeui-menu-005-accent);
border-bottom:2px solid var(--vibeui-menu-005-accent);
transform:rotate(45deg);
}
[data-vibeui-block="menu-005"] label:has(input:checked){font-weight:650}
[data-vibeui-block="menu-005"] label:has(input:focus-visible){outline:2px solid var(--vibeui-menu-005-accent);outline-offset:-2px}
[data-vibeui-block="menu-005"] [data-part="divider"]{
height:1px;margin:0.25rem 0.25rem;background:var(--vibeui-menu-005-border);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="menu-005"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_VIEWS = ["Сетка", "Список", "Таблица"]
const DEFAULT_TOGGLES = ["Показывать описания", "Только мои", "Скрыть архив"]

/**
 * Меню настроек: переключатели и выбор вида, меню не закрывается по нажатию.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Menu005({
  label = "Вид каталога",
  views = DEFAULT_VIEWS,
  toggles = DEFAULT_TOGGLES,
  onChange,
  accent,
  className,
  style,
  ...props
}: Menu005Props) {
  const id = useId()
  const [view, setView] = useState(views[0])
  const [on, setOn] = useState<string[]>([toggles[0]])

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

  const update = (next: { view?: string; on?: string[] }) => {
    const state = { view: next.view ?? view, on: next.on ?? on }
    setView(state.view)
    setOn(state.on)
    onChange?.(state)
  }

  return (
    <>
      <style href="vibeui-menu-005" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="menu-005"
        role="menu"
        aria-label={label}
        className={className}
        style={palette}
      >
        <p data-part="label">{label}</p>
        {views.map((item) => (
          <label key={item} role="menuitemradio" aria-checked={view === item}>
            <input
              type="radio"
              name={id}
              checked={view === item}
              onChange={() => update({ view: item })}
            />
            <span data-part="mark" aria-hidden="true" />
            {item}
          </label>
        ))}
        <div data-part="divider" role="separator" />
        {toggles.map((item) => (
          <label
            key={item}
            role="menuitemcheckbox"
            aria-checked={on.includes(item)}
          >
            <input
              type="checkbox"
              checked={on.includes(item)}
              onChange={() =>
                update({
                  on: on.includes(item)
                    ? on.filter((entry) => entry !== item)
                    : [...on, item],
                })
              }
            />
            <span data-part="mark" aria-hidden="true" />
            {item}
          </label>
        ))}
      </div>
    </>
  )
}