Button Group

Attached Actions

A set of related actions that reads as one part: exactly one border on the container, with inner separators drawn by the neighbours' inline-start border.

  • buttongroup
  • toolbar
  • actions
  • attached

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-002?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 "buttongroup-002" (Attached Actions) 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-002.json

Registry item: https://vibeui.ru/r/buttongroup-002.json
Installs to: components/vibeui/buttongroup-002.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 set of related actions that reads as one part: exactly one border on the container, with inner separators drawn by the neighbours' inline-start border.

A cluster of equal actions with one shared border and inner separators. Zero dependencies, one file, a server component.

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

<Buttongroup002
  actions={["Copy", "Duplicate", "Export"]}
  label="Document actions"
  size="regular"
/>

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-002-* palette — do not swap it for your theme tokens
- the single border on the container: a border per button doubles the line at every joint
- the separator via button + button — binding it to the neighbour survives any number of buttons
- calc(radius - 1px) on the edge buttons: without the subtraction the inner corner sticks out of the outer one
- the z-index and outline-offset:-2px on focus — otherwise the container border clips the ring
- role="group" with aria-label: unnamed, the three buttons are read out as three unrelated actions

## 6. You may change
- the set of captions through actions
- the accessible group name through label
- the button height through size
- the focus ring accent through accent

## 7. Rules
- The group is for equal actions: a primary button does not belong here, priority is unreadable inside a cluster.
- More than four buttons in a row do not fit on a phone — move the rest into a menu.
- Captions do not wrap: a long word stretches the group instead of folding it into two lines.
- 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-002.json
https://vibeui.ru/r/buttongroup-002.json

Компонент самодостаточен: один файл, без зависимостей, собственная палитра в переменных --vibeui-buttongroup-002-*. Серверный: хуков нет. Приём — единственная рамка на контейнере role="group" и разделители через button + button {border-inline-start}: два соседних border дали бы линию в два пикселя. Скругления детей считаются как calc(radius - 1px), чтобы внутренний угол совпал с внешним. Кнопка с фокусом поднимается z-index:1, обводка ставится внутрь через outline-offset:-2px, иначе рамка контейнера её обрезает. Высота вынесена в --vibeui-buttongroup-002-height и переключается атрибутом data-size.

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 Buttongroup002Props = Omit<
  ComponentPropsWithoutRef<"div">,
  "children"
> & {
  actions?: string[]
  label?: string
  size?: "compact" | "regular"
  accent?: string
}

// Идея компонента: группа связанных действий выглядит одной деталью, а не
// тремя кнопками рядом. Рамка ровно одна — на контейнере; разделители внутри
// рисуются border-inline-start у всех детей, кроме первого. Так между
// соседями не бывает двойной линии, а скругления берутся с контейнера через
// overflow:hidden — детям radius задавать не нужно вовсе.
const STYLES = `
:where([data-vibeui-block="buttongroup-002"]){
--vibeui-buttongroup-002-surface:oklch(1 0 0);
--vibeui-buttongroup-002-fg:oklch(0.27 0.016 265);
--vibeui-buttongroup-002-muted:oklch(0.52 0.014 265);
--vibeui-buttongroup-002-border:oklch(0.88 0.008 265);
--vibeui-buttongroup-002-hover:oklch(0.965 0.004 265);
--vibeui-buttongroup-002-press:oklch(0.93 0.006 265);
--vibeui-buttongroup-002-accent:oklch(0.55 0.17 265);
--vibeui-buttongroup-002-radius:0.625rem;
--vibeui-buttongroup-002-height:2.25rem;
--vibeui-buttongroup-002-font:ui-sans-serif,system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif;
}
[data-vibeui-block="buttongroup-002"]{
box-sizing:border-box;display:inline-flex;isolation:isolate;
border:1px solid var(--vibeui-buttongroup-002-border);
border-radius:var(--vibeui-buttongroup-002-radius);
background:var(--vibeui-buttongroup-002-surface);
font-family:var(--vibeui-buttongroup-002-font);
box-shadow:0 1px 2px oklch(0.2 0.02 265 / 6%);
}
[data-vibeui-block="buttongroup-002"] *{box-sizing:border-box}
[data-vibeui-block="buttongroup-002"][data-size="compact"]{--vibeui-buttongroup-002-height:1.875rem}
[data-vibeui-block="buttongroup-002"] button{
appearance:none;cursor:pointer;font:inherit;
position:relative;z-index:0;
display:inline-flex;align-items:center;justify-content:center;
height:var(--vibeui-buttongroup-002-height);padding:0 0.875rem;
border:0;background:transparent;
color:var(--vibeui-buttongroup-002-muted);
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-002"] button + button{
border-inline-start:1px solid var(--vibeui-buttongroup-002-border);
}
[data-vibeui-block="buttongroup-002"] button:first-child{
border-start-start-radius:calc(var(--vibeui-buttongroup-002-radius) - 1px);
border-end-start-radius:calc(var(--vibeui-buttongroup-002-radius) - 1px);
}
[data-vibeui-block="buttongroup-002"] button:last-child{
border-start-end-radius:calc(var(--vibeui-buttongroup-002-radius) - 1px);
border-end-end-radius:calc(var(--vibeui-buttongroup-002-radius) - 1px);
}
[data-vibeui-block="buttongroup-002"] button:hover{
background:var(--vibeui-buttongroup-002-hover);
color:var(--vibeui-buttongroup-002-fg);
}
[data-vibeui-block="buttongroup-002"] button:active{background:var(--vibeui-buttongroup-002-press)}
/* Кнопка с фокусом поднимается над соседями: рамка группы и разделители
   иначе срезают обводку по краям. */
[data-vibeui-block="buttongroup-002"] button:focus-visible{
z-index:1;
outline:2px solid var(--vibeui-buttongroup-002-accent);outline-offset:-2px;
color:var(--vibeui-buttongroup-002-fg);
}
@media (prefers-reduced-motion:reduce){[data-vibeui-block="buttongroup-002"] *{animation:none!important;transition:none!important}}
`

const DEFAULT_ACTIONS = ["Скопировать", "Продублировать", "Экспорт"]

/**
 * Группа действий с одной общей рамкой и разделителями внутри.
 * Один файл, ноль зависимостей, собственная палитра.
 */
export function Buttongroup002({
  actions = DEFAULT_ACTIONS,
  label = "Действия над документом",
  size = "regular",
  accent,
  className,
  style,
  ...props
}: Buttongroup002Props) {
  const palette = {
    ...(accent ? { "--vibeui-buttongroup-002-accent": accent } : null),
    ...style,
  } as CSSProperties

  return (
    <>
      <style href="vibeui-buttongroup-002" precedence="medium">
        {STYLES}
      </style>
      <div
        {...props}
        data-vibeui-block="buttongroup-002"
        data-size={size}
        role="group"
        aria-label={label}
        className={className}
        style={palette}
      >
        {actions.map((action) => (
          <button key={action} type="button">
            {action}
          </button>
        ))}
      </div>
    </>
  )
}