Grupo de botones
Combina botones divididos, barras de herramientas y complementos de entrada en una sola unidad enmarcada: un borde grueso, una sombra marcada y una pulsación compartida.
"use client"
import * as React from "react"El grupo de botones fusiona botones, campos y etiquetas adyacentes en un único control segmentado. No hay ninguna primitiva debajo: un contenedor simple con role="group" sostiene el marco neobrutalista (un borde grueso, una sombra dura y una animación de pulsación compartida); solo ButtonGroupSeparator recurre a una primitiva, envolviendo Separator.
Úsalo cuando:
- Botones divididos — una acción principal más un
DropdownMenude alternativas: Guardar / Guardar como…, Merge / Squash. - Paginadores y barras de herramientas — Anterior/Siguiente, acercar/alejar, selectores de vista agrupados en un solo marco.
- Complementos de campos — un campo de búsqueda con su botón de envío, una etiqueta con el prefijo de la URL, un botón de copiar como sufijo.
Instalación#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/button-group.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/button-group.json
Instala las siguientes dependencias:
pnpm add @base-ui/reactnpm install @base-ui/reactyarn add @base-ui/reactbun add @base-ui/react
Copia y pega el siguiente código en tu proyecto.
import { cva, type VariantProps } from "class-variance-authority"
import { Slot } from "radix-ui"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
cn(
// The GROUP itself is the cohesive NeoBrutalist unit: a single bold border +
// one hard offset shadow wrapping every segment, so it reads as ONE unit.
// The frame animates so the whole group can press like a single Button.
"relative inline-flex w-fit items-stretch border-2 border-foreground bg-background shadow-md transition-all duration-200",
// Segments become flush fills — the frame owns the border, shadow and corners.
"[&>*]:rounded-none",
"[&>:is(button,a)]:border-0 [&>:is(button,a)]:shadow-none!",
// Segments never translate on their own — only the whole frame moves.
"[&>:is(button,a)]:hover:translate-y-0! [&>:is(button,a)]:active:translate-x-0! [&>:is(button,a)]:active:translate-y-0!",
// The WHOLE group presses into its shadow like a single Button: hover sinks it
// toward the shadow, press sits it flush (shadow gone).
"has-[:is(button,a):hover]:translate-y-1 has-[:is(button,a):hover]:shadow",
"has-[:is(button,a):active]:translate-y-2 has-[:is(button,a):active]:translate-x-1 has-[:is(button,a):active]:shadow-none",
// When a group CONTAINS nested groups it becomes a plain gapped container
// (no frame / shadow / press) so each nested sub-group is its own pressable
// unit instead of pressing the whole outer frame.
"has-[>[data-slot=button-group]]:gap-2 has-[>[data-slot=button-group]]:border-0 has-[>[data-slot=button-group]]:bg-transparent has-[>[data-slot=button-group]]:shadow-none! has-[>[data-slot=button-group]]:translate-x-0! has-[>[data-slot=button-group]]:translate-y-0!",
// The hovered/pressed segment darkens in place via a full-bleed inset tint, so
// you can still tell which segment you're acting on while the frame presses.
"[&>:is(button,a):hover]:shadow-[inset_0_0_0_999px_#00000012]! [&>:is(button,a):active]:shadow-[inset_0_0_0_999px_#0000001f]!",
"dark:[&>:is(button,a):hover]:shadow-[inset_0_0_0_999px_#ffffff1f]! dark:[&>:is(button,a):active]:shadow-[inset_0_0_0_999px_#ffffff33]!",
// Solid dark segments (secondary) always lighten on hover — a dark tint would
// be invisible on a black button in light mode.
"[&>:is(button,a)[data-variant=secondary]:hover]:shadow-[inset_0_0_0_999px_#ffffff2b]! [&>:is(button,a)[data-variant=secondary]:active]:shadow-[inset_0_0_0_999px_#ffffff45]!",
"[&>:is(button,a):focus-visible]:relative [&>:is(button,a):focus-visible]:z-10"
),
{
variants: {
orientation: {
// A single crisp seam between segments (logical side, so RTL-correct).
horizontal:
"flex-row [&>*:not(:first-child)]:border-s-2! [&>*:not(:first-child)]:border-foreground",
vertical:
"flex-col [&>*:not(:first-child)]:border-t-2! [&>*:not(:first-child)]:border-foreground",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation ?? "horizontal"}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "div"
return (
<Comp
data-slot="button-group-text"
className={cn(
"inline-flex items-center gap-2 bg-muted px-3 font-head text-sm font-medium text-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"relative z-10 self-stretch bg-border data-horizontal:h-0.5 data-horizontal:w-auto data-vertical:h-auto data-vertical:w-0.5",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}
Actualiza las rutas de importación para que coincidan con la estructura de tu proyecto.
Uso#
import {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
} from "@/components/ui/button-group"<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>Composición#
Usa la siguiente composición para construir un ButtonGroup:
ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupTextButtonGroup vs ToggleGroup#
- Usa el componente
ButtonGroupcuando quieras agrupar botones que ejecutan una acción. - Usa el componente
ToggleGroupcuando quieras agrupar botones que alternan un estado.
Ejemplos#
Orientación#
Define la prop orientation para cambiar la disposición del grupo de botones.
import { MinusIcon, PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Tamaño#
Controla el tamaño de los botones con la prop size en cada botón.
import { PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Anidados#
Anida componentes <ButtonGroup> para crear grupos de botones con espaciado.
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Separador#
El componente ButtonGroupSeparator divide visualmente los botones dentro de un grupo.
Los botones con la variante outline no necesitan separador porque ya tienen un borde. Para las demás variantes, se recomienda un separador para mejorar la jerarquía visual.
import { Button } from "@/components/ui/button"
import {
ButtonGroup,Dividido#
Crea un grupo de botones dividido añadiendo dos botones separados por un ButtonGroupSeparator.
import { IconPlus } from "@tabler/icons-react"
import { Button } from "@/components/ui/button"Input#
Rodea un componente Input con botones.
import { SearchIcon } from "lucide-react"
import { Button } from "@/components/ui/button"Input Group#
Rodea un componente InputGroup para crear diseños de entrada complejos.
"use client"
import * as React from "react"Dropdown Menu#
Crea un grupo de botones dividido con un componente DropdownMenu.
"use client"
import {Select#
Combínalo con un componente Select.
"use client"
import * as React from "react"Popover#
Úsalo con un componente Popover.
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/components/ui/button"RTL#
Para activar la compatibilidad con RTL en Neobrutalism, consulta la guía de configuración de RTL.
"use client"
import * as React from "react"Accesibilidad#
ButtonGroup renderiza role="group", lo que indica a las tecnologías de asistencia que los botones van juntos sin cambiar el comportamiento del teclado: cada botón conserva su propia parada de tabulación y su activación nativa con Enter / Space. Da nombre al grupo con aria-label o aria-labelledby para que los lectores de pantalla anuncien el contexto:
<ButtonGroup aria-label="Pagination">
<Button>Previous</Button>
<Button>Next</Button>
</ButtonGroup>ButtonGroupSeparator envuelve el Separator de Base UI, que se renderiza con role="separator" y el aria-orientation correspondiente: se anuncia como un divisor y nunca recibe el foco. Si necesitas una sola parada de tabulación con navegación por flechas entre los botones, eso es el patrón Toolbar de WAI-ARIA: este componente mantiene a propósito el orden de tabulación normal.
Referencia de API#
ButtonGroup#
El componente ButtonGroup es un contenedor que agrupa botones relacionados con un estilo coherente.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" |
<ButtonGroup>
<Button>Button 1</Button>
<Button>Button 2</Button>
</ButtonGroup>Anida varios grupos de botones para crear diseños complejos con espaciado. Consulta el ejemplo anidados para más detalles.
<ButtonGroup>
<ButtonGroup />
<ButtonGroup />
</ButtonGroup>ButtonGroupSeparator#
El componente ButtonGroupSeparator divide visualmente los botones dentro de un grupo.
| Prop | Type | Default |
|---|---|---|
orientation | "horizontal" | "vertical" | "vertical" |
<ButtonGroup>
<Button>Button 1</Button>
<ButtonGroupSeparator />
<Button>Button 2</Button>
</ButtonGroup>ButtonGroupText#
Usa este componente para mostrar texto dentro de un grupo de botones.
| Prop | Type | Default |
|---|---|---|
asChild | boolean | false |
<ButtonGroup>
<ButtonGroupText>Text</ButtonGroupText>
<Button>Button</Button>
</ButtonGroup>Usa la prop asChild para renderizar un componente personalizado como texto, por ejemplo una etiqueta.
import { ButtonGroupText } from "@/components/ui/button-group"
import { Label } from "@/components/ui/label"
export function ButtonGroupTextDemo() {
return (
<ButtonGroup>
<ButtonGroupText asChild>
<Label htmlFor="name">Text</Label>
</ButtonGroupText>
<Input placeholder="Type something here..." id="name" />
</ButtonGroup>
)
}