Champ
Champs de libellé, de contrôle, d'aide et d'erreur pour les panneaux de paramètres, les formulaires de paiement et les processus d'inscription — avec un style de mise en page « néobrutaliste ».
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {Le champ de formulaire regroupe une étiquette, un contrôle, un texte d’aide et un message d’erreur dans un seul bloc aligné, et passe à l’échelle des sections groupées avec FieldGroup et FieldSet. Aucune primitive en dessous — une sémantique fieldset, legend et role="group" toute simple, composée avec Label et Separator, habillée selon la recette néobrutaliste : typographie affirmée sur les légendes et les étiquettes, tandis que les contrôles enveloppés apportent les bordures épaisses et les ombres franches.
À privilégier pour :
- Pages de réglages — empilez les sections profil, notifications et facturation avec des légendes
FieldSetet unFieldSeparatorentre les groupes. - Formulaires de paiement et d’inscription — gardez l’alignement des étiquettes, des descriptions et des erreurs cohérent entre champs, listes déroulantes et interrupteurs.
- Cartes de choix — enveloppez boutons radio, cases à cocher ou interrupteurs dans des cartes
FieldLabelsélectionnables pour les sélecteurs d’offre et les panneaux de préférences.
Installation#
pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/field.jsonnpx shadcn@latest add https://neobrutalism.com/r/base/field.jsonyarn dlx shadcn@latest add https://neobrutalism.com/r/base/field.jsonbunx --bun shadcn@latest add https://neobrutalism.com/r/base/field.json
Copiez-collez le code suivant dans votre projet.
"use client"
import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
<fieldset
data-slot="field-set"
className={cn(
"flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
className
)}
{...props}
/>
)
}
function FieldLegend({
className,
variant = "legend",
...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
return (
<legend
data-slot="field-legend"
data-variant={variant}
className={cn(
"mb-1.5 font-head font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
className
)}
{...props}
/>
)
}
function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-group"
className={cn(
"group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
className
)}
{...props}
/>
)
}
const fieldVariants = cva(
"group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
{
variants: {
orientation: {
vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
horizontal:
"flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
responsive:
"flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
},
},
defaultVariants: {
orientation: "vertical",
},
}
)
function Field({
className,
orientation = "vertical",
...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
return (
<div
role="group"
data-slot="field"
data-orientation={orientation}
className={cn(fieldVariants({ orientation }), className)}
{...props}
/>
)
}
function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-content"
className={cn(
"group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
className
)}
{...props}
/>
)
}
function FieldLabel({
className,
...props
}: React.ComponentProps<typeof Label>) {
return (
<Label
data-slot="field-label"
className={cn(
"group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded has-[>[data-slot=field]]:border-2 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
"has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
className
)}
{...props}
/>
)
}
function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="field-label"
className={cn(
"flex w-fit items-center gap-2 text-sm font-head font-medium group-data-[disabled=true]/field:opacity-50",
className
)}
{...props}
/>
)
}
function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
return (
<p
data-slot="field-description"
className={cn(
"text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
"last:mt-0 nth-last-2:-mt-1",
"[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
className
)}
{...props}
/>
)
}
function FieldSeparator({
children,
className,
...props
}: React.ComponentProps<"div"> & {
children?: React.ReactNode
}) {
return (
<div
data-slot="field-separator"
data-content={!!children}
className={cn(
"relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
className
)}
{...props}
>
<Separator className="absolute inset-0 top-1/2" />
{children && (
<span
className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
data-slot="field-separator-content"
>
{children}
</span>
)}
</div>
)
}
function FieldError({
className,
children,
errors,
...props
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>
}) {
const content = useMemo(() => {
if (children) {
return children
}
if (!errors?.length) {
return null
}
const uniqueErrors = [
...new Map(errors.map((error) => [error?.message, error])).values(),
]
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={index}>{error.message}</li>
)}
</ul>
)
}, [children, errors])
if (!content) {
return null
}
return (
<div
role="alert"
data-slot="field-error"
className={cn("text-sm font-normal text-destructive", className)}
{...props}
>
{content}
</div>
)
}
export {
Field,
FieldLabel,
FieldDescription,
FieldError,
FieldGroup,
FieldLegend,
FieldSeparator,
FieldSet,
FieldContent,
FieldTitle,
}
Adaptez les chemins d’import à la structure de votre projet.
Utilisation#
import {
Field,
FieldContent,
FieldDescription,
FieldError,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/components/ui/field"<FieldSet>
<FieldLegend>Profile</FieldLegend>
<FieldDescription>This appears on invoices and emails.</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="name">Full name</FieldLabel>
<Input id="name" autoComplete="off" placeholder="Evil Rabbit" />
<FieldDescription>This appears on invoices and emails.</FieldDescription>
</Field>
<Field>
<FieldLabel htmlFor="username">Username</FieldLabel>
<Input id="username" autoComplete="off" aria-invalid />
<FieldError>Choose another username.</FieldError>
</Field>
<Field orientation="horizontal">
<Switch id="newsletter" />
<FieldLabel htmlFor="newsletter">Subscribe to the newsletter</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>Composition#
Field#
Un contrôle unique avec libellé, texte d’aide et validation.
Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldErrorFieldGroup#
Des champs liés dans un même groupe. Insérez un FieldSeparator entre les sections au besoin.
FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
├── FieldSeparator
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectFieldSet#
Un regroupement sémantique avec une légende et une description, contenant généralement un FieldGroup.
FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectAnatomie#
La famille Field est conçue pour composer des formulaires accessibles. Un champ classique se structure ainsi :
<Field>
<FieldLabel htmlFor="input-id">Label</FieldLabel>
{/* Input, Select, Switch, etc. */}
<FieldDescription>Optional helper text.</FieldDescription>
<FieldError>Validation message.</FieldError>
</Field>Fieldest le conteneur central d’un champ unique.FieldContentest une colonne flex qui regroupe le libellé et la description. Inutile si vous n’avez pas de description.- Regroupez les champs liés avec
FieldGroup, et utilisezFieldSetavecFieldLegendpour un regroupement sémantique.
Formulaire#
Consultez la documentation Form pour créer des formulaires avec le composant Field et React Hook Form, Tanstack Form ou Formisch.
Exemples#
Input#
import {
Field,
FieldDescription,Textarea#
import {
Field,
FieldDescription,Select#
import {
Field,
FieldDescription,Slider#
"use client"
import * as React from "react"Fieldset#
import {
Field,
FieldDescription,Checkbox#
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,Radio#
import {
Field,
FieldDescription,Switch#
import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"
Carte de choix#
Enveloppez des composants Field dans un FieldLabel pour créer des groupes de champs sélectionnables. Cela fonctionne avec les composants RadioItem, Checkbox et Switch.
import {
Field,
FieldContent,Groupe de champs#
Empilez des composants Field avec FieldGroup. Ajoutez un FieldSeparator pour les séparer.
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,RTL#
Pour activer le RTL dans Neobrutalism, consultez le guide de configuration RTL.
"use client"
import * as React from "react"Mise en page responsive#
- Champs verticaux : l’orientation par défaut empile le libellé, le contrôle et le texte d’aide — idéal pour les mises en page pensées d’abord pour le mobile.
- Champs horizontaux : définissez
orientation="horizontal"surFieldpour aligner le libellé et le contrôle côte à côte. Associez-le àFieldContentpour garder les descriptions alignées. - Champs responsives : définissez
orientation="responsive"pour des dispositions en colonnes automatiques dans des parents sensibles au conteneur. Appliquez les classes@container/field-groupsurFieldGrouppour changer d’orientation à des points de rupture précis.
import { Button } from "@/components/ui/button"
import {
Field,Validation et erreurs#
- Ajoutez
data-invalidàFieldpour faire basculer tout le bloc en état d’erreur. - Ajoutez
aria-invalidsur le champ lui-même pour les technologies d’assistance. - Affichez
FieldErrorjuste après le contrôle ou à l’intérieur deFieldContentpour garder les messages d’erreur alignés avec le champ.
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>Accessibilité#
La famille Field fait primer la sémantique : FieldSet et FieldLegend rendent des fieldset/legend natifs, Field rend role="group", et FieldError rend role="alert", si bien que les messages de validation sont annoncés dès qu’ils apparaissent.
- Associez chaque
FieldLabelà son contrôle viahtmlFor/id— cliquer sur l’étiquette donne le focus au contrôle et les lecteurs d’écran annoncent l’association. - En cas de saisie invalide, définissez
aria-invalidsur le contrôle lui-même ;data-invalidsurFieldne pilote que le style d’erreur. - Le comportement clavier appartient aux contrôles internes — les enveloppes n’ajoutent ni arrêt de focus ni raccourci qui leur soit propre.
Référence API#
FieldSet#
Conteneur qui rend un fieldset sémantique avec des espacements prédéfinis.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSet>
<FieldLegend>Delivery</FieldLegend>
<FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>FieldLegend#
Élément de légende pour un FieldSet. Passez à la variante label pour l’aligner sur la taille des libellés.
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
className | string |
<FieldLegend variant="label">Notification Preferences</FieldLegend>FieldLegend propose deux variantes : legend et label. La variante label applique la taille et l’alignement d’un libellé. Pratique en cas de FieldSet imbriqués.
FieldGroup#
Conteneur de mise en page qui empile les composants Field et active les container queries pour des orientations responsives.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldGroup className="@container/field-group flex flex-col gap-6">
<Field>{/* ... */}</Field>
<Field>{/* ... */}</Field>
</FieldGroup>Field#
Le conteneur central d’un champ unique. Il gère l’orientation, le style de l’état invalide et les espacements.
| Prop | Type | Default |
|---|---|---|
orientation | "vertical" | "horizontal" | "responsive" | "vertical" |
className | string | |
data-invalid | boolean |
<Field orientation="horizontal">
<FieldLabel htmlFor="remember">Remember me</FieldLabel>
<Switch id="remember" />
</Field>FieldContent#
Colonne flex qui regroupe le contrôle et les descriptions lorsque le libellé se place à côté du contrôle. Inutile si vous n’avez pas de description.
| Prop | Type | Default |
|---|---|---|
className | string |
<Field>
<Checkbox id="notifications" />
<FieldContent>
<FieldLabel htmlFor="notifications">Notifications</FieldLabel>
<FieldDescription>Email, SMS, and push options.</FieldDescription>
</FieldContent>
</Field>FieldLabel#
Libellé stylé aussi bien pour les champs directs que pour les enfants Field imbriqués.
| Prop | Type | Default |
|---|---|---|
className | string | |
asChild | boolean | false |
<FieldLabel htmlFor="email">Email</FieldLabel>FieldTitle#
Affiche un titre avec le style d’un libellé à l’intérieur de FieldContent.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldContent>
<FieldTitle>Enable Touch ID</FieldTitle>
<FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>FieldDescription#
Emplacement de texte d’aide qui équilibre automatiquement les longues lignes dans les mises en page horizontales.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldDescription>We never share your email with anyone.</FieldDescription>FieldSeparator#
Séparateur visuel pour distinguer des sections à l’intérieur d’un FieldGroup. Accepte un contenu en ligne facultatif.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSeparator>Or continue with</FieldSeparator>FieldError#
Conteneur d’erreur accessible qui accepte des enfants ou un tableau errors (par exemple issu de react-hook-form).
| Prop | Type | Default |
|---|---|---|
errors | Array<{ message?: string } | undefined> | |
className | string |
<FieldError errors={errors.username} />Lorsque le tableau errors contient plusieurs messages, le composant affiche automatiquement une liste.
FieldError accepte aussi les problèmes produits par tout validateur qui implémente Standard Schema, notamment Zod, Valibot et ArkType. Passez directement le tableau issues du résultat du schéma pour afficher une liste d’erreurs unifiée entre les différentes bibliothèques.