Saltar al contenido

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 DropdownMenu de 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.json
npx shadcn@latest add https://neobrutalism.com/r/base/button-group.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/button-group.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/button-group.json

Instala las siguientes dependencias:

pnpm add @base-ui/react
npm install @base-ui/react
yarn add @base-ui/react
bun add @base-ui/react

Copia y pega el siguiente código en tu proyecto.

components/ui/button-group.tsx
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
└── ButtonGroupText

ButtonGroup vs ToggleGroup

  • Usa el componente ButtonGroup cuando quieras agrupar botones que ejecutan una acción.
  • Usa el componente ToggleGroup cuando 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"

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.

PropTypeDefault
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.

PropTypeDefault
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.

PropTypeDefault
asChildbooleanfalse
<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>
  )
}