Bidang
Kolom label, kontrol, teks bantuan, dan pesan kesalahan untuk panel pengaturan, formulir pembayaran, dan alur pendaftaran — dengan gaya desain formulir neobrutalis.
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {Field membungkus label, kontrol, teks bantuan, dan pesan kesalahan menjadi satu blok yang rapi sejajar, lalu dapat diskalakan menjadi bagian-bagian terkelompok dengan FieldGroup dan FieldSet. Tidak ada primitif di baliknya — semantik fieldset, legend, dan role="group" polos yang dikomposisikan dengan Label dan Separator, ditata dengan resep neobrutalis: tipografi judul yang tegas pada legend dan label, sementara kontrol yang dibungkus membawa garis tepi tebal dan bayangan pekatnya.
Gunakan komponen ini saat:
- Halaman pengaturan — susun bagian profil, notifikasi, dan penagihan dengan legend
FieldSetsertaFieldSeparatordi antara grup. - Formulir checkout dan pendaftaran — jaga kesejajaran label, deskripsi, dan pesan kesalahan tetap konsisten di seluruh input, select, dan switch.
- Kartu pilihan — bungkus radio, checkbox, atau switch dalam kartu
FieldLabelyang dapat dipilih untuk pemilih paket dan panel preferensi.
Instalasi#
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
Salin dan tempel kode berikut ke dalam proyek Anda.
"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,
}
Perbarui jalur impor agar sesuai dengan pengaturan proyek Anda.
Penggunaan#
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>Komposisi#
Field#
Satu kontrol dengan label, teks bantuan, dan validasi.
Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldErrorFieldGroup#
Field terkait dalam satu grup. Gunakan FieldSeparator antar bagian bila diperlukan.
FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
├── FieldSeparator
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectFieldSet#
Pengelompokan semantik dengan legenda dan deskripsi, biasanya berisi FieldGroup.
FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectAnatomi#
Keluarga Field dirancang untuk menyusun formulir yang mudah diakses. Field pada umumnya disusun sebagai berikut:
<Field>
<FieldLabel htmlFor="input-id">Label</FieldLabel>
{/* Input, Select, Switch, etc. */}
<FieldDescription>Optional helper text.</FieldDescription>
<FieldError>Validation message.</FieldError>
</Field>Fieldadalah pembungkus inti untuk satu field.FieldContentadalah kolom flex yang mengelompokkan label dan deskripsi. Tidak diperlukan jika Anda tidak memiliki deskripsi.- Bungkus field terkait dengan
FieldGroup, dan gunakanFieldSetdenganFieldLegenduntuk pengelompokan semantik.
Formulir#
Lihat dokumentasi Form untuk membangun formulir dengan komponen Field dan React Hook Form, Tanstack Form, atau Formisch.
Contoh#
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"
Kartu Pilihan#
Bungkus komponen Field di dalam FieldLabel untuk membuat grup field yang dapat dipilih. Ini bekerja dengan komponen RadioItem, Checkbox, dan Switch.
import {
Field,
FieldContent,Grup Field#
Tumpuk komponen Field dengan FieldGroup. Tambahkan FieldSeparator untuk membaginya.
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,RTL#
Untuk mengaktifkan dukungan RTL di Neobrutalism, lihat panduan konfigurasi RTL.
"use client"
import * as React from "react"Tata Letak Responsif#
- Field vertikal: Orientasi default menumpuk label, kontrol, dan teks bantuan — ideal untuk tata letak mobile-first.
- Field horizontal: Setel
orientation="horizontal"padaFielduntuk menyelaraskan label dan kontrol secara berdampingan. Pasangkan denganFieldContentuntuk menjaga deskripsi tetap sejajar. - Field responsif: Setel
orientation="responsive"untuk tata letak kolom otomatis di dalam elemen induk yang menyadari kontainer. Terapkan kelas@container/field-grouppadaFieldGroupuntuk mengganti orientasi pada breakpoint tertentu.
import { Button } from "@/components/ui/button"
import {
Field,Validasi dan Kesalahan#
- Tambahkan
data-invalidkeFielduntuk mengubah seluruh blok menjadi status kesalahan. - Tambahkan
aria-invalidpada input itu sendiri untuk teknologi bantu. - Render
FieldErrorsegera setelah kontrol atau di dalamFieldContentuntuk menjaga pesan kesalahan tetap sejajar dengan field.
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>Aksesibilitas#
Keluarga field mengutamakan semantik: FieldSet dan FieldLegend merender fieldset/legend natif, Field merender role="group", dan FieldError merender role="alert" sehingga pesan validasi diumumkan begitu muncul.
- Pasangkan setiap
FieldLabeldengan kontrolnya lewathtmlFor/id— mengklik label memfokuskan kontrol dan pembaca layar mengumumkan keterkaitannya. - Saat input tidak valid, setel
aria-invalidpada kontrolnya sendiri;data-invalidpadaFieldhanya menggerakkan gaya tampilan kesalahan. - Perilaku keyboard sepenuhnya milik kontrol di dalamnya — pembungkus ini tidak menambahkan perhentian fokus atau ikatan tombol apa pun.
Referensi API#
FieldSet#
Kontainer yang merender fieldset semantik dengan preset spasi.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSet>
<FieldLegend>Delivery</FieldLegend>
<FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>FieldLegend#
Elemen legenda untuk FieldSet. Beralih ke varian label untuk menyelaraskan dengan ukuran label.
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
className | string |
<FieldLegend variant="label">Notification Preferences</FieldLegend>FieldLegend memiliki dua varian: legend dan label. Varian label menerapkan ukuran dan perataan label. Berguna jika Anda memiliki FieldSet bersarang.
FieldGroup#
Pembungkus tata letak yang menumpuk komponen Field dan mengaktifkan container query untuk orientasi responsif.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldGroup className="@container/field-group flex flex-col gap-6">
<Field>{/* ... */}</Field>
<Field>{/* ... */}</Field>
</FieldGroup>Field#
Pembungkus inti untuk satu field. Menyediakan kontrol orientasi, penataan gaya status tidak valid, dan spasi.
| 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#
Kolom flex yang mengelompokkan kontrol dan deskripsi saat label berada di samping kontrol. Tidak diperlukan jika Anda tidak memiliki deskripsi.
| 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 yang ditata untuk input langsung maupun anak Field bersarang.
| Prop | Type | Default |
|---|---|---|
className | string | |
asChild | boolean | false |
<FieldLabel htmlFor="email">Email</FieldLabel>FieldTitle#
Merender judul dengan penataan gaya label di dalam FieldContent.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldContent>
<FieldTitle>Enable Touch ID</FieldTitle>
<FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>FieldDescription#
Slot teks bantuan yang secara otomatis menyeimbangkan baris panjang dalam tata letak horizontal.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldDescription>We never share your email with anyone.</FieldDescription>FieldSeparator#
Pembatas visual untuk memisahkan bagian di dalam FieldGroup. Menerima konten inline opsional.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSeparator>Or continue with</FieldSeparator>FieldError#
Kontainer kesalahan yang mudah diakses yang menerima anak atau array errors (misalnya, dari react-hook-form).
| Prop | Type | Default |
|---|---|---|
errors | Array<{ message?: string } | undefined> | |
className | string |
<FieldError errors={errors.username} />Ketika array errors berisi beberapa pesan, komponen merender daftar secara otomatis.
FieldError juga menerima masalah yang dihasilkan oleh validator apa pun yang mengimplementasikan Standard Schema, termasuk Zod, Valibot, dan ArkType. Teruskan array issues dari hasil skema secara langsung untuk merender daftar kesalahan terpadu di berbagai pustaka.