Toggle Group

List View Group

A view switcher together with the list it controls: the link is declared through aria-controls, and the three views are three rule sets over one piece of markup.

  • togglegroup
  • view
  • aria-controls
  • layout

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/togglegroup-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 "togglegroup-003" (List View Group) 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/togglegroup-003.json

Registry item: https://vibeui.ru/r/togglegroup-003.json
Installs to: components/vibeui/togglegroup-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 view switcher together with the list it controls: the link is declared through aria-controls, and the three views are three rule sets over one piece of markup.

A list-view switcher together with the list itself: aria-controls ties them, and the view changes through rules over one markup. One file, zero dependencies.

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

<Togglegroup003
  label="List view"
  defaultValue="list"
  items={["Brief", "Estimate", "Contract"]}
/>

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-togglegroup-003-* palette — do not swap it for your theme tokens
- aria-controls on the list: the link between buttons and region must not exist only in the design
- the unchanged list markup: rebuilding it loses scroll position and focus
- aria-pressed as a declaration of the view state, not as a highlight of the active button
- role="group" with a name — otherwise the three buttons sound random
- the panel's own light surface: dark text must bring its own background

## 6. You may change
- the group name through label
- the initial view through defaultValue
- the list contents through items
- the selected colour through accent

## 7. Rules
- Views are described by CSS rules keyed on data-view: adding a fourth one means adding its rules.
- In the compact view captions are truncated with an ellipsis — that is the price of density.
- The state lives in the tab's memory: to survive a reload, keep it outside the component.
- 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/togglegroup-003.json
https://vibeui.ru/r/togglegroup-003.json

Один файл, ноль зависимостей, палитра --vibeui-togglegroup-003-*. Клиентский: useState и useId. Группа не абстрактная: она отдаёт aria-controls на конкретный список и меняет его вид атрибутом data-view на корне. Список при этом не пересобирается — меняются только правила раскладки, поэтому переключение не теряет прокрутку и фокус внутри. Это togglegroup, а не buttongroup: кнопка не запускает действие, а объявляет состояние вида через aria-pressed, и ровно одно из трёх состояний включено.

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 Togglegroup003Props = Omit<
  ComponentPropsWithoutRef<"section">,
  "children" | "onChange"
> & {
  label?: string
  defaultValue?: string
  items?: string[]
  onChange?: (value: string) => void
  accent?: string
}

// Идея компонента: группа переключателей вместе с тем, чем она управляет.
// aria-controls связывает её со списком, и вид меняется в той же разметке —
// видно, что группа выбирает состояние вида, а не запускает действие.
const STYLES = `
:where([data-vibeui-block="togglegroup-003"]){
--vibeui-togglegroup-003-bg:oklch(1 0 0);
--vibeui-togglegroup-003-fg:oklch(0.22 0.014 265);
--vibeui-togglegroup-003-muted:oklch(0.55 0.014 265);
--vibeui-togglegroup-003-border:oklch(0.9 0.006 265);
--vibeui-togglegroup-003-surface:oklch(0.97 0.004 265);
--vibeui-togglegroup-003-accent:oklch(0.56 0.15 195);
--vibeui-togglegroup-003-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="togglegroup-003"]{
box-sizing:border-box;display:flex;flex-direction:column;gap:0.75rem;
width:100%;max-width:26rem;padding:0.875rem;
border:1px solid var(--vibeui-togglegroup-003-border);border-radius:0.875rem;
background:var(--vibeui-togglegroup-003-bg);color:var(--vibeui-togglegroup-003-fg);
font-family:var(--vibeui-togglegroup-003-font);
}
[data-vibeui-block="togglegroup-003"] *{box-sizing:border-box}
[data-vibeui-block="togglegroup-003"] [data-part="head"]{
display:flex;align-items:center;justify-content:space-between;gap:0.75rem;flex-wrap:wrap;
}
[data-vibeui-block="togglegroup-003"] h3{margin:0;font-size:0.875rem;font-weight:650}
[data-vibeui-block="togglegroup-003"] [data-part="group"]{
display:inline-flex;gap:0.125rem;padding:0.1875rem;
border-radius:0.625rem;background:var(--vibeui-togglegroup-003-surface);
}
[data-vibeui-block="togglegroup-003"] button{
appearance:none;cursor:pointer;font:inherit;
display:inline-flex;align-items:center;gap:0.375rem;
height:1.875rem;padding:0 0.5625rem;border:0;border-radius:0.4375rem;
background:transparent;color:var(--vibeui-togglegroup-003-muted);
font-size:0.75rem;font-weight:600;line-height:1;
transition:background-color .15s ease,color .15s ease;
}
[data-vibeui-block="togglegroup-003"] button svg{width:0.875rem;height:0.875rem}
[data-vibeui-block="togglegroup-003"] button:focus-visible{
outline:2px solid var(--vibeui-togglegroup-003-accent);outline-offset:1px;
}
[data-vibeui-block="togglegroup-003"] button[aria-pressed="true"]{
background:var(--vibeui-togglegroup-003-bg);color:var(--vibeui-togglegroup-003-accent);
box-shadow:0 1px 2px oklch(0.2 0.02 265 / 14%);
}
[data-vibeui-block="togglegroup-003"] ul{
list-style:none;margin:0;padding:0;display:grid;gap:0.375rem;
}
[data-vibeui-block="togglegroup-003"] li{
display:flex;align-items:center;gap:0.5rem;
padding:0.5rem 0.625rem;border-radius:0.5rem;
background:var(--vibeui-togglegroup-003-surface);
font-size:0.8125rem;line-height:1.3;
}
[data-vibeui-block="togglegroup-003"] li::before{
content:"";flex:none;width:1.5rem;height:1.5rem;border-radius:0.375rem;
background:color-mix(in oklab,var(--vibeui-togglegroup-003-accent) 22%,white);
}
/* Три вида — три набора правил над одной и той же разметкой: список не
   пересобирается, поэтому переключение не теряет прокрутку и фокус. */
[data-vibeui-block="togglegroup-003"][data-view="grid"] ul{grid-template-columns:repeat(2,minmax(0,1fr))}
[data-vibeui-block="togglegroup-003"][data-view="grid"] li{
flex-direction:column;align-items:flex-start;gap:0.4375rem;padding:0.625rem;
}
[data-vibeui-block="togglegroup-003"][data-view="grid"] li::before{width:100%;height:2.25rem}
[data-vibeui-block="togglegroup-003"][data-view="compact"] ul{gap:0}
[data-vibeui-block="togglegroup-003"][data-view="compact"] li{
padding:0.3125rem 0.5rem;border-radius:0;background:transparent;
border-bottom:1px solid var(--vibeui-togglegroup-003-border);font-size:0.75rem;
}
[data-vibeui-block="togglegroup-003"][data-view="compact"] li::before{width:0.375rem;height:0.375rem;border-radius:50%}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="togglegroup-003"] *{animation:none!important;transition:none!important}}
`

const VIEWS = [
  { id: "list", label: "Список", d: "M2 4h10M2 7h10M2 10h10" },
  {
    id: "grid",
    label: "Сетка",
    d: "M2 2.5h4v4H2zM8 2.5h4v4H8zM2 8.5h4v4H2zM8 8.5h4v4H8z",
  },
  { id: "compact", label: "Компактно", d: "M2 3h10M2 5.5h10M2 8h10M2 10.5h10" },
]

const DEFAULT_ITEMS = [
  "Бриф для дизайнера",
  "Смета на печать",
  "Договор подряда",
  "Отчёт за квартал",
]

/**
 * Группа выбора вида списка, связанная со списком через aria-controls:
 * вид меняется правилами над одной разметкой. Один файл, ноль зависимостей.
 */
export function Togglegroup003({
  label = "Вид списка",
  defaultValue = "list",
  items = DEFAULT_ITEMS,
  onChange,
  accent,
  className,
  style,
  ...props
}: Togglegroup003Props) {
  const [value, setValue] = useState(defaultValue)
  const listId = useId()

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

  return (
    <>
      <style href="vibeui-togglegroup-003" precedence="medium">
        {STYLES}
      </style>
      <section
        {...props}
        data-vibeui-block="togglegroup-003"
        data-view={value}
        className={className}
        style={palette}
      >
        <div data-part="head">
          <h3>Документы</h3>
          <div data-part="group" role="group" aria-label={label}>
            {VIEWS.map((view) => (
              <button
                key={view.id}
                type="button"
                aria-pressed={value === view.id}
                aria-controls={listId}
                onClick={() => {
                  setValue(view.id)
                  onChange?.(view.id)
                }}
              >
                <svg viewBox="0 0 14 14" fill="none" aria-hidden="true">
                  <path
                    d={view.d}
                    stroke="currentColor"
                    strokeWidth="1.3"
                    strokeLinecap="round"
                  />
                </svg>
                {view.label}
              </button>
            ))}
          </div>
        </div>
        <ul id={listId}>
          {items.map((item) => (
            <li key={item}>{item}</li>
          ))}
        </ul>
      </section>
    </>
  )
}