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 undFieldSeparatorzwischen 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.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
Kopiere den folgenden Code und füge ihn in dein Projekt ein.
"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
└── FieldErrorFieldGroup#
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 / SelectFieldSet#
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 / SelectAnatomie#
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>Fieldist der Kern-Wrapper für ein einzelnes Feld.FieldContentist eine Flex-Spalte, die Beschriftung und Beschreibung gruppiert. Nicht erforderlich, wenn du keine Beschreibung hast.- Umschließe zusammengehörige Felder mit
FieldGroupund verwendeFieldSetmitFieldLegendfü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"anField, um Beschriftung und Steuerelement nebeneinander auszurichten. Kombiniere mitFieldContent, um Beschreibungen ausgerichtet zu halten. - Responsive Felder: Setze
orientation="responsive"für automatische Spaltenlayouts innerhalb container-bewusster Elternelemente. Wende@container/field-group-Klassen aufFieldGroupan, um die Ausrichtung an bestimmten Breakpoints zu wechseln.
import { Button } from "@/components/ui/button"
import {
Field,Validierung und Fehler#
- Füge
data-invalidzuFieldhinzu, um den gesamten Block in einen Fehlerzustand zu versetzen. - Füge
aria-invalidan der Eingabe selbst für assistive Technologien hinzu. - Rendere
FieldErrorunmittelbar nach dem Steuerelement oder innerhalb vonFieldContent, 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überhtmlFor/idmit seinem Steuerelement – ein Klick auf das Label fokussiert das Steuerelement, und Screenreader kündigen die Zuordnung an. - Setze bei ungültiger Eingabe
aria-invalidauf dem Steuerelement selbst;data-invalidaufFieldsteuert 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.
| Prop | Type | Default |
|---|---|---|
className | string |
<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.
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
className | string |
<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.
| Prop | Type | Default |
|---|---|---|
className | string |
<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.
| 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-Spalte, die Steuerelement und Beschreibungen gruppiert, wenn die Beschriftung neben dem Steuerelement sitzt. Nicht erforderlich, wenn du keine Beschreibung hast.
| 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#
Beschriftung, die sowohl für direkte Eingaben als auch für verschachtelte Field-Kinder gestaltet ist.
| Prop | Type | Default |
|---|---|---|
className | string | |
asChild | boolean | false |
<FieldLabel htmlFor="email">Email</FieldLabel>FieldTitle#
Rendert einen Titel mit Beschriftungs-Styling innerhalb von FieldContent.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldContent>
<FieldTitle>Enable Touch ID</FieldTitle>
<FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>FieldDescription#
Hilfetext-Slot, der lange Zeilen in horizontalen Layouts automatisch ausbalanciert.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldDescription>We never share your email with anyone.</FieldDescription>FieldSeparator#
Visueller Trenner, um Abschnitte innerhalb einer FieldGroup zu trennen. Akzeptiert optionalen Inline-Inhalt.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSeparator>Or continue with</FieldSeparator>FieldError#
Barrierefreier Fehlercontainer, der Kinder oder ein errors-Array akzeptiert (z. B. von react-hook-form).
| Prop | Type | Default |
|---|---|---|
errors | Array<{ message?: string } | undefined> | |
className | string |
<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.