Groupe de champs
Ajoutez des icônes, des boutons et des infobulles aux champs de saisie des barres de recherche, des champs d'URL et des fenêtres de discussion, avec des bordures et des ombres de style néobrutaliste.
import { Search } from "lucide-react"
import {Le groupe de saisie enveloppe un champ ou une zone de texte avec ses compléments — icônes, texte en ligne, boutons, rappels de raccourcis — dans un seul contrôle bordé. C’est une composition maison des composants Input, Textarea et Button de cette bibliothèque, habillée selon la recette néobrutaliste : bordures épaisses, ombres franches et typographie affirmée.
À privilégier pour :
- Champs de recherche — une icône de recherche en tête, plus un rappel
⌘Kou un bouton d’effacement en queue. - Saisies préfixées et suffixées — un texte fixe
https://,@ou.compour que l’utilisateur ne tape que la partie variable. - Compositeurs de chat et de prompt — une zone de texte avec une barre d’outils
block-endde boutons et une action d’envoi.
Installation#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/input-group.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/input-group.json
Copiez-collez le code suivant dans votre projet.
"use client"
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="input-group"
role="group"
className={cn(
"group/input-group relative flex h-8 w-full min-w-0 items-center rounded border-2 bg-input shadow-sm transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:outline-2 has-[[data-slot=input-group-control]:focus-visible]:outline-offset-2 has-[[data-slot=input-group-control]:focus-visible]:outline-primary has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
className
)}
{...props}
/>
)
}
const inputGroupAddonVariants = cva(
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
{
variants: {
align: {
"inline-start":
"order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
"inline-end":
"order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
"block-start":
"order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
"block-end":
"order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
},
},
defaultVariants: {
align: "inline-start",
},
}
)
function InputGroupAddon({
className,
align = "inline-start",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
return (
<div
role="group"
data-slot="input-group-addon"
data-align={align}
className={cn(inputGroupAddonVariants({ align }), className)}
onClick={(e) => {
if ((e.target as HTMLElement).closest("button")) {
return
}
e.currentTarget.parentElement?.querySelector("input")?.focus()
}}
{...props}
/>
)
}
const inputGroupButtonVariants = cva(
"flex items-center gap-2 text-sm shadow-none",
{
variants: {
size: {
xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
sm: "",
"icon-xs":
"size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
"icon-sm": "size-8 p-0 has-[>svg]:p-0",
},
},
defaultVariants: {
size: "xs",
},
}
)
function InputGroupButton({
className,
type = "button",
variant = "ghost",
size = "xs",
...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
VariantProps<typeof inputGroupButtonVariants>) {
return (
<Button
type={type}
data-size={size}
variant={variant}
className={cn(inputGroupButtonVariants({ size }), className)}
{...props}
/>
)
}
function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
className={cn(
"flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function InputGroupInput({
className,
...props
}: React.ComponentProps<"input">) {
return (
<Input
data-slot="input-group-control"
className={cn(
"flex-1 rounded-none border-0 bg-transparent shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
function InputGroupTextarea({
className,
...props
}: React.ComponentProps<"textarea">) {
return (
<Textarea
data-slot="input-group-control"
className={cn(
"flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
className
)}
{...props}
/>
)
}
export {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupText,
InputGroupInput,
InputGroupTextarea,
}
Adaptez les chemins d’import à la structure de votre projet.
Utilisation#
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/components/ui/input-group"<InputGroup>
<InputGroupInput placeholder="Search..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>Composition#
Utilisez la composition suivante pour construire un InputGroup :
InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
├── InputGroupButton
└── InputGroupTextAlignement#
Utilisez la prop align de InputGroupAddon pour positionner le complément par rapport au champ de saisie.
Pour une bonne gestion du focus, InputGroupAddon doit toujours être placé
après InputGroupInput ou InputGroupTextarea dans le DOM. Utilisez la prop
align pour positionner visuellement le complément.
inline-start#
Utilisez align="inline-start" pour placer le complément au début du champ de saisie. C’est la valeur par défaut.
import { SearchIcon } from "lucide-react"
import {inline-end#
Utilisez align="inline-end" pour placer le complément à la fin du champ de saisie.
import { EyeOffIcon } from "lucide-react"
import {block-start#
Utilisez align="block-start" pour placer le complément au-dessus du champ de saisie.
import { CopyIcon, FileCodeIcon } from "lucide-react"
import {block-end#
Utilisez align="block-end" pour placer le complément sous le champ de saisie.
import {
Field,
FieldDescription,Exemples#
Icône#
import {
CheckIcon,
CreditCardIcon,Texte#
import {
InputGroup,
InputGroupAddon,Bouton#
"use client"
import * as React from "react"Kbd#
import { SearchIcon } from "lucide-react"
import {Menu déroulant#
"use client"
import { ChevronDownIcon, MoreHorizontal } from "lucide-react"Spinner#
import { LoaderIcon } from "lucide-react"
import {Textarea#
import {
IconBrandJavascript,
IconCopy,Champ personnalisé#
Ajoutez l’attribut data-slot="input-group-control" à votre champ personnalisé pour une gestion automatique de l’état de focus.
Voici un exemple de zone de texte personnalisée et redimensionnable provenant d’une bibliothèque tierce.
"use client"
import TextareaAutosize from "react-textarea-autosize"RTL#
Pour activer le RTL dans Neobrutalism, consultez le guide de configuration RTL.
"use client"
import * as React from "react"Accessibilité#
Il n’existe pas de modèle de widget ARIA pour les groupes de saisie — l’enveloppe rend role="group" pour que les technologies d’assistance associent les compléments au contrôle, et l’accès clavier est natif : Tab atteint le champ puis chaque InputGroupButton dans l’ordre du DOM, raison pour laquelle les compléments se placent après le champ (et se positionnent visuellement avec align). Deux points restent à votre charge : un texte de complément comme https:// est visuel, pas une étiquette programmatique, le champ a donc toujours besoin d’un <Label> ou d’un aria-label ; et les boutons à icône seule exigent un aria-label — cliquer sur un complément donne le focus au champ, mais c’est une commodité au pointeur, pas un chemin clavier.
Référence API#
InputGroup#
Le composant principal qui enveloppe les champs de saisie et les compléments.
| Prop | Type | Default |
|---|---|---|
className | string |
<InputGroup>
<InputGroupInput />
<InputGroupAddon />
</InputGroup>InputGroupAddon#
Affiche des icônes, du texte, des boutons ou d’autres contenus à côté des champs de saisie.
Pour une bonne navigation au focus, le composant InputGroupAddon doit être
placé après le champ de saisie. Définissez la prop align pour positionner le
complément.
| Prop | Type | Default |
|---|---|---|
align | "inline-start" | "inline-end" | "block-start" | "block-end" | "inline-start" |
className | string |
<InputGroupAddon align="inline-end">
<SearchIcon />
</InputGroupAddon>Pour <InputGroupInput />, utilisez l’alignement inline-start ou inline-end. Pour <InputGroupTextarea />, utilisez l’alignement block-start ou block-end.
Le composant InputGroupAddon peut contenir plusieurs composants InputGroupButton et plusieurs icônes.
<InputGroupAddon>
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>InputGroupButton#
Affiche des boutons à l’intérieur des groupes de champs.
| Prop | Type | Default |
|---|---|---|
size | "xs" | "icon-xs" | "sm" | "icon-sm" | "xs" |
variant | "default" | "destructive" | "outline" | "secondary" | "ghost" | "link" | "ghost" |
className | string |
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton size="icon-xs" aria-label="Copy">
<CopyIcon />
</InputGroupButton>InputGroupInput#
Remplace <Input /> lors de la création de groupes de champs. Ce composant applique déjà les styles du groupe de champs et utilise le data-slot="input-group-control" unifié pour la gestion de l’état de focus.
| Prop | Type | Default |
|---|---|---|
className | string |
Toutes les autres props sont transmises au composant <Input /> sous-jacent.
<InputGroup>
<InputGroupInput placeholder="Enter text..." />
<InputGroupAddon>
<SearchIcon />
</InputGroupAddon>
</InputGroup>InputGroupTextarea#
Remplace <Textarea /> lors de la création de groupes de champs. Ce composant applique déjà les styles du groupe de zone de texte et utilise le data-slot="input-group-control" unifié pour la gestion de l’état de focus.
| Prop | Type | Default |
|---|---|---|
className | string |
Toutes les autres props sont transmises au composant <Textarea /> sous-jacent.
<InputGroup>
<InputGroupTextarea placeholder="Enter message..." />
<InputGroupAddon align="block-end">
<InputGroupButton>Send</InputGroupButton>
</InputGroupAddon>
</InputGroup>