Zum Inhalt springen

Feld

Bezeichnungen, Steuerelemente, Hilfetexte und Fehlermeldungen für Einstellungsfelder, Checkout-Formulare und Anmeldeabläufe – im neobrutalistischen Formularstil.

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

Das Field bündelt Label, Steuerelement, Hilfetext und Fehlermeldung zu einem sauber ausgerichteten Block und skaliert mit FieldGroup und FieldSet bis zu gruppierten Abschnitten. Darunter liegt kein Primitive – schlichte fieldset-, legend- und role="group"-Semantik, komponiert mit Label und Separator und nach dem neobrutalistischen Rezept gestaltet: kräftige Überschriften-Typografie auf Legenden und Labels, während die umschlossenen Steuerelemente die dicken Rahmen und harten Schatten mitbringen.

Typische Anwendungsfälle:

  • Einstellungsseiten – staple Profil-, Benachrichtigungs- und Abrechnungsabschnitte mit FieldSet-Legenden und FieldSeparator zwischen den Gruppen.
  • Checkout- und Registrierungsformulare – halte Label, Beschreibung und Fehlermeldung über Inputs, Selects und Switches hinweg einheitlich ausgerichtet.
  • Auswahlkarten – verpacke Radios, Checkboxen oder Switches in auswählbare FieldLabel-Karten für Tarifauswahl und Einstellungs-Panels.

Installation

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

Kopiere den folgenden Code und füge ihn in dein Projekt ein.

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

Passe die Importpfade an deine Projektkonfiguration an.

Verwendung

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>

Komposition

Field

Ein einzelnes Steuerelement mit Beschriftung, Hilfetext und Validierung.

Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldError

FieldGroup

Zusammengehörige Felder in einer Gruppe. Verwende bei Bedarf FieldSeparator zwischen Abschnitten.

FieldGroup
├── Field
│   ├── FieldLabel
│   ├── Input / Textarea / Switch / Select
│   ├── FieldDescription
│   └── FieldError
├── FieldSeparator
└── Field
    ├── FieldLabel
    └── Input / Textarea / Switch / Select

FieldSet

Semantische Gruppierung mit einer Legende und einer Beschreibung, die üblicherweise eine FieldGroup enthält.

FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
    ├── Field
    │   ├── FieldLabel
    │   ├── Input / Textarea / Switch / Select
    │   ├── FieldDescription
    │   └── FieldError
    └── Field
        ├── FieldLabel
        └── Input / Textarea / Switch / Select

Anatomie

Die Field-Familie ist für das Zusammensetzen barrierefreier Formulare konzipiert. Ein typisches Feld ist wie folgt aufgebaut:

<Field>
  <FieldLabel htmlFor="input-id">Label</FieldLabel>
  {/* Input, Select, Switch, etc. */}
  <FieldDescription>Optional helper text.</FieldDescription>
  <FieldError>Validation message.</FieldError>
</Field>
  • Field ist der Kern-Wrapper für ein einzelnes Feld.
  • FieldContent ist eine Flex-Spalte, die Beschriftung und Beschreibung gruppiert. Nicht erforderlich, wenn du keine Beschreibung hast.
  • Umschließe zusammengehörige Felder mit FieldGroup und verwende FieldSet mit FieldLegend für die semantische Gruppierung.

Formular

Siehe die Form-Dokumentation zum Erstellen von Formularen mit der Field-Komponente und React Hook Form, Tanstack Form oder Formisch.

Beispiele

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"

Auswahlkarte

Umschließe Field-Komponenten mit FieldLabel, um auswählbare Feldgruppen zu erstellen. Das funktioniert mit den Komponenten RadioItem, Checkbox und Switch.

import {
  Field,
  FieldContent,

Feldgruppe

Staple Field-Komponenten mit FieldGroup. Füge FieldSeparator hinzu, um sie zu trennen.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,

RTL

Um RTL-Unterstützung in Neobrutalism zu aktivieren, siehe die RTL-Konfigurationsanleitung.

"use client"

import * as React from "react"

Responsives Layout

  • Vertikale Felder: Die Standardausrichtung stapelt Beschriftung, Steuerelement und Hilfetext — ideal für Mobile-First-Layouts.
  • Horizontale Felder: Setze orientation="horizontal" an Field, um Beschriftung und Steuerelement nebeneinander auszurichten. Kombiniere mit FieldContent, um Beschreibungen ausgerichtet zu halten.
  • Responsive Felder: Setze orientation="responsive" für automatische Spaltenlayouts innerhalb container-bewusster Elternelemente. Wende @container/field-group-Klassen auf FieldGroup an, um die Ausrichtung an bestimmten Breakpoints zu wechseln.
import { Button } from "@/components/ui/button"
import {
  Field,

Validierung und Fehler

  • Füge data-invalid zu Field hinzu, um den gesamten Block in einen Fehlerzustand zu versetzen.
  • Füge aria-invalid an der Eingabe selbst für assistive Technologien hinzu.
  • Rendere FieldError unmittelbar nach dem Steuerelement oder innerhalb von FieldContent, um Fehlermeldungen am Feld ausgerichtet zu halten.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid />
  <FieldError>Enter a valid email address.</FieldError>
</Field>

Barrierefreiheit

Die Field-Familie setzt zuerst auf Semantik: FieldSet und FieldLegend rendern natives fieldset/legend, Field rendert role="group", und FieldError rendert role="alert", sodass Validierungsmeldungen im Moment ihres Erscheinens angekündigt werden.

  • Verknüpfe jedes FieldLabel über htmlFor/id mit seinem Steuerelement – ein Klick auf das Label fokussiert das Steuerelement, und Screenreader kündigen die Zuordnung an.
  • Setze bei ungültiger Eingabe aria-invalid auf dem Steuerelement selbst; data-invalid auf Field steuert nur das Fehler-Styling.
  • Das Tastaturverhalten gehört den Steuerelementen im Inneren – die Wrapper fügen weder eigene Fokus-Stopps noch eigene Tastenbelegungen hinzu.

API-Referenz

FieldSet

Container, der ein semantisches fieldset mit Abstands-Voreinstellungen rendert.

PropTypeDefault
classNamestring
<FieldSet>
  <FieldLegend>Delivery</FieldLegend>
  <FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>

FieldLegend

Legenden-Element für ein FieldSet. Wechsle zur Variante label, um sie an die Beschriftungsgröße anzupassen.

PropTypeDefault
variant"legend" | "label""legend"
classNamestring
<FieldLegend variant="label">Notification Preferences</FieldLegend>

FieldLegend hat zwei Varianten: legend und label. Die Variante label wendet Beschriftungsgröße und -ausrichtung an. Praktisch, wenn du verschachtelte FieldSet hast.

FieldGroup

Layout-Wrapper, der Field-Komponenten stapelt und Container-Queries für responsive Ausrichtungen aktiviert.

PropTypeDefault
classNamestring
<FieldGroup className="@container/field-group flex flex-col gap-6">
  <Field>{/* ... */}</Field>
  <Field>{/* ... */}</Field>
</FieldGroup>

Field

Der Kern-Wrapper für ein einzelnes Feld. Bietet Ausrichtungssteuerung, Styling für den ungültigen Zustand und Abstände.

PropTypeDefault
orientation"vertical" | "horizontal" | "responsive""vertical"
classNamestring
data-invalidboolean
<Field orientation="horizontal">
  <FieldLabel htmlFor="remember">Remember me</FieldLabel>
  <Switch id="remember" />
</Field>

FieldContent

Flex-Spalte, die Steuerelement und Beschreibungen gruppiert, wenn die Beschriftung neben dem Steuerelement sitzt. Nicht erforderlich, wenn du keine Beschreibung hast.

PropTypeDefault
classNamestring
<Field>
  <Checkbox id="notifications" />
  <FieldContent>
    <FieldLabel htmlFor="notifications">Notifications</FieldLabel>
    <FieldDescription>Email, SMS, and push options.</FieldDescription>
  </FieldContent>
</Field>

FieldLabel

Beschriftung, die sowohl für direkte Eingaben als auch für verschachtelte Field-Kinder gestaltet ist.

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

FieldTitle

Rendert einen Titel mit Beschriftungs-Styling innerhalb von FieldContent.

PropTypeDefault
classNamestring
<FieldContent>
  <FieldTitle>Enable Touch ID</FieldTitle>
  <FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>

FieldDescription

Hilfetext-Slot, der lange Zeilen in horizontalen Layouts automatisch ausbalanciert.

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

FieldSeparator

Visueller Trenner, um Abschnitte innerhalb einer FieldGroup zu trennen. Akzeptiert optionalen Inline-Inhalt.

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

FieldError

Barrierefreier Fehlercontainer, der Kinder oder ein errors-Array akzeptiert (z. B. von react-hook-form).

PropTypeDefault
errorsArray<{ message?: string } | undefined>
classNamestring
<FieldError errors={errors.username} />

Wenn das errors-Array mehrere Meldungen enthält, rendert die Komponente automatisch eine Liste.

FieldError akzeptiert auch Probleme, die von jedem Validator erzeugt werden, der Standard Schema implementiert, einschließlich Zod, Valibot und ArkType. Übergib das issues-Array aus dem Schema-Ergebnis direkt, um eine einheitliche Fehlerliste über alle Bibliotheken hinweg zu rendern.