Naar inhoud

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 een FieldSeparator tussen 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.json
npx shadcn@latest add https://neobrutalism.com/r/base/field.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/field.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/field.json

Kopieer en plak de volgende code in je project.

components/ui/field.tsx
"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
└── FieldError

FieldGroup

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 / Select

FieldSet

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 / Select

Anatomie

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>
  • Field is de kernwrapper voor één veld.
  • FieldContent is een flex-kolom die label en beschrijving groepeert. Niet vereist als je geen beschrijving hebt.
  • Omhul gerelateerde velden met FieldGroup en gebruik FieldSet met FieldLegend voor 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 op Field om het label en besturingselement naast elkaar uit te lijnen. Combineer met FieldContent om beschrijvingen uitgelijnd te houden.
  • Responsieve velden: Stel orientation="responsive" in voor automatische kolomlay-outs binnen container-bewuste ouderelementen. Pas @container/field-group-klassen toe op FieldGroup om oriëntaties te wisselen op specifieke breekpunten.
import { Button } from "@/components/ui/button"
import {
  Field,

Validatie en fouten

  • Voeg data-invalid toe aan Field om het hele blok naar een foutstatus te schakelen.
  • Voeg aria-invalid toe aan het invoerveld zelf voor ondersteunende technologieën.
  • Render FieldError direct na het besturingselement of binnen FieldContent om 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 FieldLabel via htmlFor/id aan zijn besturingselement — klikken op het label geeft het element focus en schermlezers benoemen het verband.
  • Zet bij ongeldige invoer aria-invalid op het besturingselement zelf; data-invalid op Field stuurt 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.

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

PropTypeDefault
variant"legend" | "label""legend"
classNamestring
<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.

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

PropTypeDefault
orientation"vertical" | "horizontal" | "responsive""vertical"
classNamestring
data-invalidboolean
<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.

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

PropTypeDefault
classNamestring
asChildbooleanfalse
<FieldLabel htmlFor="email">Email</FieldLabel>

FieldTitle

Rendert een titel met labelstyling binnen FieldContent.

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

PropTypeDefault
classNamestring
<FieldDescription>We never share your email with anyone.</FieldDescription>

FieldSeparator

Visuele scheidingslijn om secties binnen een FieldGroup te scheiden. Accepteert optionele inline-inhoud.

PropTypeDefault
classNamestring
<FieldSeparator>Or continue with</FieldSeparator>

FieldError

Toegankelijke foutcontainer die onderliggende elementen of een errors-array accepteert (bijv. van react-hook-form).

PropTypeDefault
errorsArray<{ message?: string } | undefined>
classNamestring
<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.