Button Group

Expanding Extras

A cluster that reveals rare actions to the right: details is laid out as a flex row, so the summary stays a cell of the group rather than a block heading.

  • buttongroup
  • details
  • progressive
  • toolbar

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/buttongroup-035?lang=en

More
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 "buttongroup-035" (Expanding Extras) 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/buttongroup-035.json

Registry item: https://vibeui.ru/r/buttongroup-035.json
Installs to: components/vibeui/buttongroup-035.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 cluster that reveals rare actions to the right: details is laid out as a flex row, so the summary stays a cell of the group rather than a block heading.

An action group that expands rare buttons to the right on details/summary. Zero dependencies, one file, a server component.

## 3. How to use it
import { Buttongroup035 } from "@/components/vibeui/buttongroup-035"

<Buttongroup035
  actions={["Reply", "Forward"]}
  extra={["Archive", "Spam"]}
  moreLabel="More"
  label="Message actions"
/>

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-buttongroup-035-* palette — do not swap it for your theme tokens
- display:flex on the details itself: without it the revealed buttons drop below the summary
- details/summary instead of hand-rolled disclosure — state and keyboard come from the browser
- handing the rounded corner from the summary to the last button when open: otherwise the right edge breaks off
- removing the list marker with all three rules — Safari and Chrome hide it differently
- the z-index on focus: there is one border on the container and the ring would otherwise sit under it

## 6. You may change
- the primary actions through actions
- the rare actions through extra
- the disclosure caption through moreLabel and the group name through label
- the open-state and ring colour through accent

## 7. Rules
- details does not close on an outside click: normal for a menu, expected for a cluster.
- Expanding pushes the row neighbours aside: reserve the space in a crowded header.
- The buttons carry no onClick: wire your handlers at install time.
- 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/buttongroup-035.json
https://vibeui.ru/r/buttongroup-035.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-035-*. Серверный: раскрытие держит details/summary, клиентского JS нет — состояние, клавиатура и объявление «свёрнуто/развёрнуто» приходят от браузера. Приём — display:flex на самом details: тогда summary встаёт ячейкой сцепки, а раскрытые кнопки выстраиваются справа от него в общий ряд, а не блоком под ним. Маркер списка убран через list-style:none, ::marker и ::-webkit-details-marker. Пока details закрыт, скругление правого края отдаётся summary, при открытии — последней дополнительной кнопке. Стрелка поворачивается по [open].

Component source

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

import type { ComponentPropsWithoutRef, CSSProperties } from "react"

export type Buttongroup035Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  actions?: string[]
  extra?: string[]
  moreLabel?: string
  label?: string
  accent?: string
}

// Идея компонента: редкие действия не уезжают в выпадающее меню, а
// раскрываются вправо, оставаясь частью той же сцепки. Раскрытие держит
// details/summary: состояние, клавиатура и объявление «свёрнуто/развёрнуто»
// достаются от браузера, JS не нужен. details выложен flex-строкой, поэтому
// summary остаётся кнопкой сцепки, а раскрытые кнопки встают справа от неё
// в общий ряд, а не блоком под ним. Стрелка на summary поворачивается по
// [open], маркер списка убран через ::marker и ::-webkit-details-marker.
const STYLES = `
:where([data-vibeui-block="buttongroup-035"]){
--vibeui-buttongroup-035-surface:oklch(1 0 0);
--vibeui-buttongroup-035-fg:oklch(0.25 0.016 265);
--vibeui-buttongroup-035-muted:oklch(0.55 0.014 265);
--vibeui-buttongroup-035-border:oklch(0.88 0.008 265);
--vibeui-buttongroup-035-accent:oklch(0.5 0.16 265);
--vibeui-buttongroup-035-radius:0.625rem;
--vibeui-buttongroup-035-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-035"]{
box-sizing:border-box;display:inline-flex;isolation:isolate;
border:1px solid var(--vibeui-buttongroup-035-border);
border-radius:var(--vibeui-buttongroup-035-radius);
background:var(--vibeui-buttongroup-035-surface);
font-family:var(--vibeui-buttongroup-035-font);
}
[data-vibeui-block="buttongroup-035"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-035"] button,
[data-vibeui-block="buttongroup-035"] summary{
appearance:none;cursor:pointer;font:inherit;list-style:none;
position:relative;z-index:0;
display:inline-flex;align-items:center;gap:0.375rem;
height:2.25rem;padding:0 0.8125rem;
border:0;background:transparent;
color:var(--vibeui-buttongroup-035-fg);
font-size:0.8125rem;font-weight:600;line-height:1;white-space:nowrap;
transition:background-color .16s ease,color .16s ease;
}
[data-vibeui-block="buttongroup-035"] summary::marker{content:""}
[data-vibeui-block="buttongroup-035"] summary::-webkit-details-marker{display:none}
[data-vibeui-block="buttongroup-035"] button:hover,
[data-vibeui-block="buttongroup-035"] summary:hover{background:oklch(0.965 0.005 265)}
[data-vibeui-block="buttongroup-035"] button:focus-visible,
[data-vibeui-block="buttongroup-035"] summary:focus-visible{
z-index:1;outline:2px solid var(--vibeui-buttongroup-035-accent);outline-offset:-2px;
}
[data-vibeui-block="buttongroup-035"] [data-part="base"] button + button,
[data-vibeui-block="buttongroup-035"] details,
[data-vibeui-block="buttongroup-035"] [data-part="extra"] button{
border-inline-start:1px solid var(--vibeui-buttongroup-035-border);
}
[data-vibeui-block="buttongroup-035"] [data-part="base"]{display:flex}
[data-vibeui-block="buttongroup-035"] [data-part="base"] button:first-child{
border-start-start-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
border-end-start-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
}
/* details выложен строкой: summary остаётся ячейкой сцепки. */
[data-vibeui-block="buttongroup-035"] details{display:flex;align-items:stretch}
[data-vibeui-block="buttongroup-035"] [data-part="extra"]{display:flex}
[data-vibeui-block="buttongroup-035"] [data-part="extra"] button:last-child{
border-start-end-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
border-end-end-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
}
[data-vibeui-block="buttongroup-035"] details:not([open]) summary{
border-start-end-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
border-end-end-radius:calc(var(--vibeui-buttongroup-035-radius) - 1px);
}
[data-vibeui-block="buttongroup-035"] summary{color:var(--vibeui-buttongroup-035-muted)}
[data-vibeui-block="buttongroup-035"] details[open] summary{
color:var(--vibeui-buttongroup-035-accent);background:oklch(0.97 0.02 265);
}
[data-vibeui-block="buttongroup-035"] summary svg{
width:0.875rem;height:0.875rem;
stroke:currentColor;fill:none;stroke-width:2.2;stroke-linecap:round;stroke-linejoin:round;
transition:rotate .18s ease;
}
[data-vibeui-block="buttongroup-035"] details[open] summary svg{rotate:180deg}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-035"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ACTIONS = ["Ответить", "Переслать"]
const DEFAULT_EXTRA = ["В архив", "Спам", "Напомнить"]

/**
 * Сцепка, которая раскрывает редкие действия вправо, оставаясь одной деталью.
 * Один файл, ноль зависимостей, серверный компонент на details/summary.
 */
export function Buttongroup035({
  actions = DEFAULT_ACTIONS,
  extra = DEFAULT_EXTRA,
  moreLabel = "Ещё",
  label = "Действия над письмом",
  accent,
  className,
  style,
  ...props
}: Buttongroup035Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-035-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-035" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="buttongroup-035"
        className={className}
        style={palette}
        role="group"
        aria-label={label}
      >
        <div data-part="base">
          {actions.map((action) => (
            <button key={action} type="button">
              {action}
            </button>
          ))}
        </div>
        <details>
          <summary>
            {moreLabel}
            <svg viewBox="0 0 24 24" aria-hidden="true">
              <path d="m6 9 6 6 6-6" />
            </svg>
          </summary>
          <div data-part="extra">
            {extra.map((action) => (
              <button key={action} type="button">
                {action}
              </button>
            ))}
          </div>
        </details>
      </div>
    </>
  )
}