Lewati ke konten

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 FieldSet serta FieldSeparator di 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 FieldLabel yang dapat dipilih untuk pemilih paket dan panel preferensi.

Instalasi

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

Salin dan tempel kode berikut ke dalam proyek Anda.

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

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
└── FieldError

FieldGroup

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

FieldSet

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

Anatomi

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>
  • Field adalah pembungkus inti untuk satu field.
  • FieldContent adalah kolom flex yang mengelompokkan label dan deskripsi. Tidak diperlukan jika Anda tidak memiliki deskripsi.
  • Bungkus field terkait dengan FieldGroup, dan gunakan FieldSet dengan FieldLegend untuk 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" pada Field untuk menyelaraskan label dan kontrol secara berdampingan. Pasangkan dengan FieldContent untuk 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-group pada FieldGroup untuk mengganti orientasi pada breakpoint tertentu.
import { Button } from "@/components/ui/button"
import {
  Field,

Validasi dan Kesalahan

  • Tambahkan data-invalid ke Field untuk mengubah seluruh blok menjadi status kesalahan.
  • Tambahkan aria-invalid pada input itu sendiri untuk teknologi bantu.
  • Render FieldError segera setelah kontrol atau di dalam FieldContent untuk 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 FieldLabel dengan kontrolnya lewat htmlFor/id — mengklik label memfokuskan kontrol dan pembaca layar mengumumkan keterkaitannya.
  • Saat input tidak valid, setel aria-invalid pada kontrolnya sendiri; data-invalid pada Field hanya 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.

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

FieldLegend

Elemen legenda untuk FieldSet. Beralih ke varian label untuk menyelaraskan dengan ukuran label.

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

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

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

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

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

FieldTitle

Merender judul dengan penataan gaya label di dalam FieldContent.

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

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

FieldSeparator

Pembatas visual untuk memisahkan bagian di dalam FieldGroup. Menerima konten inline opsional.

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

FieldError

Kontainer kesalahan yang mudah diakses yang menerima anak atau array errors (misalnya, dari react-hook-form).

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