Veld
Label-, invoer-, help- en foutvelden voor instellingenpanelen, afrekenformulieren en aanmeldingsprocessen — in de neobrutalistische vormgeving.
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {Het field bundelt een label, een besturingselement, hulptekst en een foutmelding in één uitgelijnd blok, en schaalt met FieldGroup en FieldSet op naar gegroepeerde secties. Er zit geen primitive onder — gewone fieldset-, legend- en role="group"-semantiek, samengesteld met Label en Separator en vormgegeven volgens het neobrutalistische recept: vette koppen op legends en labels, terwijl de omwikkelde besturingselementen zelf de dikke randen en scherpe schaduwen meebrengen.
Ideaal voor:
- Instellingenpagina’s — stapel secties voor profiel, meldingen en facturering met
FieldSet-legends en eenFieldSeparatortussen de groepen. - Checkout- en registratieformulieren — houd de uitlijning van labels, beschrijvingen en fouten consistent over inputs, selects en switches heen.
- Keuzekaarten — verpak radio’s, checkboxes of switches in selecteerbare
FieldLabel-kaarten voor abonnementskiezers en voorkeurspanelen.
Installatie#
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
Kopieer en plak de volgende code in je project.
"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,
}
Werk de importpaden bij zodat ze overeenkomen met je projectconfiguratie.
Gebruik#
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>Compositie#
Field#
Eén besturingselement met label, helptekst en validatie.
Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldErrorFieldGroup#
Gerelateerde velden in één groep. Gebruik FieldSeparator tussen secties wanneer nodig.
FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
├── FieldSeparator
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectFieldSet#
Semantische groepering met een legenda en beschrijving, meestal met een FieldGroup.
FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectAnatomie#
De Field-familie is ontworpen voor het samenstellen van toegankelijke formulieren. Een typisch veld is als volgt gestructureerd:
<Field>
<FieldLabel htmlFor="input-id">Label</FieldLabel>
{/* Input, Select, Switch, etc. */}
<FieldDescription>Optional helper text.</FieldDescription>
<FieldError>Validation message.</FieldError>
</Field>Fieldis de kernwrapper voor één veld.FieldContentis een flex-kolom die label en beschrijving groepeert. Niet vereist als je geen beschrijving hebt.- Omhul gerelateerde velden met
FieldGroupen gebruikFieldSetmetFieldLegendvoor semantische groepering.
Formulier#
Zie de Form-documentatie voor het bouwen van formulieren met het Field-component en React Hook Form, Tanstack Form of Formisch.
Voorbeelden#
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"
Keuzekaart#
Omhul Field-componenten binnen FieldLabel om selecteerbare veldgroepen te maken. Dit werkt met de componenten RadioItem, Checkbox en Switch.
import {
Field,
FieldContent,Veldgroep#
Stapel Field-componenten met FieldGroup. Voeg FieldSeparator toe om ze te scheiden.
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,RTL#
Om RTL-ondersteuning in Neobrutalism in te schakelen, zie de RTL-configuratiegids.
"use client"
import * as React from "react"Responsieve lay-out#
- Verticale velden: De standaardoriëntatie stapelt label, besturingselement en helptekst — ideaal voor mobile-first-lay-outs.
- Horizontale velden: Stel
orientation="horizontal"in opFieldom het label en besturingselement naast elkaar uit te lijnen. Combineer metFieldContentom beschrijvingen uitgelijnd te houden. - Responsieve velden: Stel
orientation="responsive"in voor automatische kolomlay-outs binnen container-bewuste ouderelementen. Pas@container/field-group-klassen toe opFieldGroupom oriëntaties te wisselen op specifieke breekpunten.
import { Button } from "@/components/ui/button"
import {
Field,Validatie en fouten#
- Voeg
data-invalidtoe aanFieldom het hele blok naar een foutstatus te schakelen. - Voeg
aria-invalidtoe aan het invoerveld zelf voor ondersteunende technologieën. - Render
FieldErrordirect na het besturingselement of binnenFieldContentom foutmeldingen uitgelijnd te houden met het veld.
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>Toegankelijkheid#
De field-familie begint bij semantiek: FieldSet en FieldLegend renderen native fieldset/legend, Field rendert role="group" en FieldError rendert role="alert", zodat validatiemeldingen worden voorgelezen op het moment dat ze verschijnen.
- Koppel elk
FieldLabelviahtmlFor/idaan zijn besturingselement — klikken op het label geeft het element focus en schermlezers benoemen het verband. - Zet bij ongeldige invoer
aria-invalidop het besturingselement zelf;data-invalidopFieldstuurt alleen de foutstyling aan. - Het toetsenbordgedrag hoort bij de besturingselementen erbinnen — de wrappers voegen zelf geen focusstops of toetsbindingen toe.
API-referentie#
FieldSet#
Container die een semantische fieldset met vooraf ingestelde afstanden rendert.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSet>
<FieldLegend>Delivery</FieldLegend>
<FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>FieldLegend#
Legenda-element voor een FieldSet. Schakel over naar de label-variant om aan te sluiten bij de labelgrootte.
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
className | string |
<FieldLegend variant="label">Notification Preferences</FieldLegend>De FieldLegend heeft twee varianten: legend en label. De label-variant past labelgrootte en -uitlijning toe. Handig als je geneste FieldSet hebt.
FieldGroup#
Lay-outwrapper die Field-componenten stapelt en containerquery's inschakelt voor responsieve oriëntaties.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldGroup className="@container/field-group flex flex-col gap-6">
<Field>{/* ... */}</Field>
<Field>{/* ... */}</Field>
</FieldGroup>Field#
De kernwrapper voor één veld. Biedt oriëntatiebeheer, styling voor de ongeldige staat en afstand.
| 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#
Flex-kolom die besturingselement en beschrijvingen groepeert wanneer het label naast het besturingselement staat. Niet vereist als je geen beschrijving hebt.
| 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#
Label opgemaakt voor zowel directe invoervelden als geneste Field-onderliggende elementen.
| Prop | Type | Default |
|---|---|---|
className | string | |
asChild | boolean | false |
<FieldLabel htmlFor="email">Email</FieldLabel>FieldTitle#
Rendert een titel met labelstyling binnen FieldContent.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldContent>
<FieldTitle>Enable Touch ID</FieldTitle>
<FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>FieldDescription#
Helptekst-slot die lange regels automatisch balanceert in horizontale lay-outs.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldDescription>We never share your email with anyone.</FieldDescription>FieldSeparator#
Visuele scheidingslijn om secties binnen een FieldGroup te scheiden. Accepteert optionele inline-inhoud.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSeparator>Or continue with</FieldSeparator>FieldError#
Toegankelijke foutcontainer die onderliggende elementen of een errors-array accepteert (bijv. van react-hook-form).
| Prop | Type | Default |
|---|---|---|
errors | Array<{ message?: string } | undefined> | |
className | string |
<FieldError errors={errors.username} />Wanneer de errors-array meerdere berichten bevat, rendert het component automatisch een lijst.
FieldError accepteert ook problemen die worden geproduceerd door elke validator die Standard Schema implementeert, waaronder Zod, Valibot en ArkType. Geef de issues-array van het schemaresultaat direct door om een uniforme foutenlijst tussen bibliotheken te renderen.