ButtonGroup
Buttons sit flush so related actions read as one control. Use for aligned toolbars, not free-floating CTAs.
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Aligned toolbar of related actions:
```tsx import { ButtonGroup, ButtonGroupSeparator } from '@/components/atoms/ButtonGroup'; import { Button } from '@/components/atoms/Button';
<ButtonGroup orientation="horizontal" aria-label="Document actions"> <Button variant="secondary" size="s">Edit</Button> <ButtonGroupSeparator /> <Button variant="secondary" size="s">Share</Button> <Button variant="primary" size="s">Publish</Button> </ButtonGroup> ```
## Accesibilidad
- Root exposes `role="group"` — always pass `aria-label` (or `aria-labelledby`) describing the set of actions. - Separators are `aria-hidden`; do not put meaning only in the divider.
### Correcto
- role='group' se agrega automaticamente al container - aria-label en el ButtonGroup describe el proposito del grupo - ButtonGroupSeparator tiene aria-hidden='true' — no se anuncia - Navegacion por teclado funciona: Tab mueve entre botones del grupo
### Evitar
- No anidar ButtonGroups — un solo nivel de agrupacion - No mezclar tamaños distintos en un mismo grupo — rompe la alineacion visual - No usar sin aria-label — el screen reader no puede describir el proposito del grupo
## Cuándo no usar
- Free-floating unrelated buttons in a page section — use spacing/layout, not `ButtonGroup`. - Single primary CTA — use one `Button`, not a group of one.
## Criterio de uso
- Agrupa acciones que operan sobre el mismo objeto o flujo y que necesitan leerse como una unidad. - Usa orientación horizontal en toolbars y vertical cuando el espacio o el contexto móvil exija acciones apiladas. - Pasa un nombre accesible al grupo; el agrupamiento visual no explica por sí solo la relación entre controles.
## Gotchas
- El grupo controla el colapso de bordes y radios. No añadas márgenes individuales entre hijos porque crearás separaciones inconsistentes. - Mantén el orden de acciones de menor a mayor compromiso y evita mezclar acciones sin relación sólo para ahorrar espacio. - **Nota**: Este CSS requiere que los botones hijos ya tengan sus estilos (Button o IconButton standalone CSS). ButtonGroup solo maneja el layout y radius collapse.
Uso
import { ButtonGroup } from '@/components/atoms/ButtonGroup';
import { Button } from '@/components/atoms/Button';
<ButtonGroup orientation="horizontal">
<Button variant="secondary">Cancel</Button>
<Button variant="primary">Save</Button>
</ButtonGroup>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| orientation | select | horizontal | `horizontal`, `vertical` | Layout axis of grouped buttons. | horizontal for toolbars; vertical for stacked mobile actions. Default: horizontal. |
Gotchas
- layout
Children must be Button/IconButton siblings — spacing is owned by the group, not ad-hoc margins.
Anatomía CSS
<div class="button-group"> <span class="button-group__separator"></span> <span class="button-group__text"></span> </div>
| Clase | Propósito |
|---|---|
button-group | root |
button-group--vertical | modifier |
button-group__separator | element |
button-group__text | element |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #f5f5f5 | muted |
| all | fg | #525252 | muted.foreground |
| all | border | 1px | stroke.hairline |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* -------------------------------------------------------------------------
Button Group
Joins adjacent buttons into a cohesive unit.
Removes inner radius, collapses inner borders.
Orientation: horizontal (default), vertical
Parts: .button-group, .button-group__separator, .button-group__text
------------------------------------------------------------------------- */
/* ---- Container ---- */
.button-group {
display: inline-flex;
align-items: stretch;
}
.button-group--vertical {
flex-direction: column;
}
/* ---- Horizontal: radius + border collapse ---- */
.button-group:not(.button-group--vertical) > .button,
.button-group:not(.button-group--vertical) > .icon-button {
border-radius: 0;
}
.button-group:not(.button-group--vertical) > .button:first-child,
.button-group:not(.button-group--vertical) > .icon-button:first-child {
border-radius: 8px 0 0 8px;
}
.button-group:not(.button-group--vertical) > .button:last-child,
.button-group:not(.button-group--vertical) > .icon-button:last-child {
border-radius: 0 8px 8px 0;
}
.button-group:not(.button-group--vertical) > .button + .button,
.button-group:not(.button-group--vertical) > .icon-button + .icon-button,
.button-group:not(.button-group--vertical) > .button + .icon-button,
.button-group:not(.button-group--vertical) > .icon-button + .button {
margin-left: calc(-1 * 1px);
}
/* ---- Vertical: radius + border collapse ---- */
.button-group--vertical > .button,
.button-group--vertical > .icon-button {
border-radius: 0;
width: 100%;
}
.button-group--vertical > .button:first-child,
.button-group--vertical > .icon-button:first-child {
border-radius: 8px 8px 0 0;
}
.button-group--vertical > .button:last-child,
.button-group--vertical > .icon-button:last-child {
border-radius: 0 0 8px 8px;
}
.button-group--vertical > .button + .button,
.button-group--vertical > .icon-button + .icon-button,
.button-group--vertical > .button + .icon-button,
.button-group--vertical > .icon-button + .button {
margin-top: calc(-1 * 1px);
}
/* ---- Separator ---- */
.button-group__separator {
align-self: stretch;
background-color: #e5e5e5;
}
.button-group:not(.button-group--vertical) > .button-group__separator {
width: 1px;
}
.button-group--vertical > .button-group__separator {
height: 1px;
}
/* ---- Text ---- */
.button-group__text {
display: inline-flex;
align-items: center;
padding: 0 12px;
font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
font-size: 12.8px;
font-weight: 500;
color: #525252;
white-space: nowrap;
border: 1px solid #e5e5e5;
background-color: #f5f5f5;
}
Codigo fuente
import { forwardRef, type ReactNode } from 'react'; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } /* ---- Group ---- */ export type ButtonGroupProps = { orientation?: 'horizontal' | 'vertical'; 'aria-label'?: string; children: ReactNode; className?: string; }; export const ButtonGroup = forwardRef<HTMLDivElement, ButtonGroupProps>( ({ orientation = 'horizontal', children, className, ...props }, ref) => { return ( <div ref={ref} role="group" className={cn('button-group', orientation === 'vertical' && 'button-group--vertical', className)} {...props} > {children} </div> ); }, ); ButtonGroup.displayName = 'ButtonGroup'; /* ---- Separator ---- */ export type ButtonGroupSeparatorProps = { className?: string; }; export function ButtonGroupSeparator({ className }: ButtonGroupSeparatorProps) { return <div aria-hidden="true" className={cn('button-group__separator', className)} />; } /* ---- Text ---- */ export type ButtonGroupTextProps = { children: ReactNode; className?: string; }; export function ButtonGroupText({ children, className }: ButtonGroupTextProps) { return <span className={cn('button-group__text', className)}>{children}</span>; }
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / ButtonGroup) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
selectoron.button-group:not(.button-group--vertical) > .button, .button-group:not(.button-group--vertical) > .icon-button— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group:not(.button-group--vertical) > .button:first-child, .button-group:not(.button-group--vertical) > .icon-button:first-child— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group:not(.button-group--vertical) > .button:last-child, .button-group:not(.button-group--vertical) > .icon-button:last-child— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group:not(.button-group--vertical) > .button + .button, .button-group:not(.button-group--vertical) > .icon-button + .icon-button, .button-group:not(.button-group--vertical) > .button + .icon-button, .button-group:not(.button-group--vertical) > .icon-button + .button— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group--vertical > .button, .button-group--vertical > .icon-button— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group--vertical > .button:first-child, .button-group--vertical > .icon-button:first-child— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group--vertical > .button:last-child, .button-group--vertical > .icon-button:last-child— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)selectoron.button-group--vertical > .button + .button, .button-group--vertical > .icon-button + .icon-button, .button-group--vertical > .button + .icon-button, .button-group--vertical > .icon-button + .button— compound/descendant selector — moved to head Custom Code (Designer styles are single-class)
Tras pegar: Create component → nombre Atom / ButtonGroup → Publish.