Aller au contenu

Groupe de boutons

Fuses regroupe les boutons, les barres d'outils et les modules complémentaires de saisie au sein d'un seul élément encadré : une bordure épaisse, une ombre portée marquée, une action commune.

"use client"

import * as React from "react"

Le groupe de boutons fusionne boutons, champs et libellés adjacents en un seul contrôle segmenté. Aucune primitive en dessous — un simple conteneur role="group" porte le cadre néobrutaliste (une bordure épaisse, une ombre marquée, une même animation d’appui partagée) ; seul ButtonGroupSeparator fait appel à une primitive, en enveloppant Separator.

À privilégier pour :

  • Boutons scindés — une action principale plus un DropdownMenu d’alternatives : Enregistrer / Enregistrer sous…, Merge / Squash.
  • Paginations et barres d’outils — Précédent/Suivant, zoom avant/arrière, sélecteurs de vue rassemblés dans un même cadre.
  • Compléments de champ — un champ de recherche avec son bouton d’envoi, un libellé de préfixe d’URL, un bouton de copie en suffixe.

Installation

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

Installez les dépendances suivantes :

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

Copiez et collez le code suivant dans votre projet.

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,
}

Adaptez les chemins d’import à la structure de votre projet.

Utilisation

import {
  ButtonGroup,
  ButtonGroupSeparator,
  ButtonGroupText,
} from "@/components/ui/button-group"
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Composition

Utilisez la composition suivante pour construire un ButtonGroup :

ButtonGroup
├── Button or Input
├── ButtonGroupSeparator
└── ButtonGroupText

ButtonGroup vs ToggleGroup

  • Utilisez le composant ButtonGroup pour regrouper des boutons qui déclenchent une action.
  • Utilisez le composant ToggleGroup pour regrouper des boutons qui basculent un état.

Exemples

Orientation

Définissez la prop orientation pour changer la disposition du groupe de boutons.

import { MinusIcon, PlusIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Taille

Contrôlez la taille des boutons à l’aide de la prop size sur chaque bouton.

import { PlusIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Imbriqués

Imbriquez des composants <ButtonGroup> pour créer des groupes de boutons espacés.

import { AudioLinesIcon, PlusIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Séparateur

Le composant ButtonGroupSeparator sépare visuellement les boutons au sein d’un groupe.

Les boutons de variante outline n’ont pas besoin de séparateur puisqu’ils ont une bordure. Pour les autres variantes, un séparateur est recommandé pour améliorer la hiérarchie visuelle.

import { Button } from "@/components/ui/button"
import {
  ButtonGroup,

Scindé

Créez un groupe de boutons scindé en ajoutant deux boutons séparés par un ButtonGroupSeparator.

import { IconPlus } from "@tabler/icons-react"

import { Button } from "@/components/ui/button"

Input

Entourez un composant Input de boutons.

import { SearchIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

Input Group

Entourez un composant InputGroup pour créer des dispositions de saisie complexes.

"use client"

import * as React from "react"

Créez un groupe de boutons scindé avec un composant DropdownMenu.

"use client"

import {

Select

Associez-le à un composant Select.

"use client"

import * as React from "react"

Popover

Utilisez-le avec un composant Popover.

import { BotIcon, ChevronDownIcon } from "lucide-react"

import { Button } from "@/components/ui/button"

RTL

Pour activer le RTL dans Neobrutalism, consultez le guide de configuration RTL.

"use client"

import * as React from "react"

Accessibilité

ButtonGroup rend role="group", ce qui signale aux technologies d’assistance que les boutons vont ensemble sans modifier le comportement clavier — chaque bouton conserve son propre arrêt de tabulation et son activation native Enter / Space. Nommez le groupe avec aria-label ou aria-labelledby pour que les lecteurs d’écran annoncent le contexte :

<ButtonGroup aria-label="Pagination">
  <Button>Previous</Button>
  <Button>Next</Button>
</ButtonGroup>

ButtonGroupSeparator enveloppe le Separator de Base UI, rendu role="separator" avec l’aria-orientation correspondante — annoncé comme séparateur, jamais focalisable. S’il vous faut un unique arrêt de tabulation avec navigation aux flèches entre les boutons, c’est le modèle WAI-ARIA Toolbar — ce composant conserve délibérément l’ordre de tabulation simple.

Référence API

ButtonGroup

Le composant ButtonGroup est un conteneur qui regroupe des boutons apparentés avec un style cohérent.

PropTypeDefault
orientation"horizontal" | "vertical""horizontal"
<ButtonGroup>
  <Button>Button 1</Button>
  <Button>Button 2</Button>
</ButtonGroup>

Imbriquez plusieurs groupes de boutons pour créer des dispositions complexes et espacées. Consultez l’exemple imbriqués pour en savoir plus.

<ButtonGroup>
  <ButtonGroup />
  <ButtonGroup />
</ButtonGroup>

ButtonGroupSeparator

Le composant ButtonGroupSeparator sépare visuellement les boutons au sein d’un groupe.

PropTypeDefault
orientation"horizontal" | "vertical""vertical"
<ButtonGroup>
  <Button>Button 1</Button>
  <ButtonGroupSeparator />
  <Button>Button 2</Button>
</ButtonGroup>

ButtonGroupText

Utilisez ce composant pour afficher du texte au sein d’un groupe de boutons.

PropTypeDefault
asChildbooleanfalse
<ButtonGroup>
  <ButtonGroupText>Text</ButtonGroupText>
  <Button>Button</Button>
</ButtonGroup>

Utilisez la prop asChild pour afficher un composant personnalisé comme texte, par exemple une étiquette.

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