پرش به محتوا

فیلد

برچسب‌ها، کنترل‌ها، متن‌های راهنما و اسلات‌های خطا برای پنل‌های تنظیمات، فرم‌های تسویه‌حساب و جریان‌های ثبت‌نام — با رویکرد فرم نئوبروتالیست.

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

Field برچسب، کنترل، متن راهنما و پیام خطا را در یک بلوک هم‌تراز می‌پیچد و با FieldGroup و FieldSet به بخش‌های گروهی مقیاس می‌شود. primitive زیرین ندارد — semantics سادهٔ fieldset، legend و role="group" ترکیب‌شده با Label و Separator، با دستور پخت نئوبروتالیستی: تایپ heading پررنگ روی legend و label، در حالی که کنترل‌های پیچیده‌شده حاشیه‌های ضخیم و سایه‌های سخت را می‌آورند.

برای این موارد از آن استفاده کنید:

  • صفحات تنظیمات — بخش‌های پروفایل، اعلان و صورتحساب را با legendهای FieldSet و FieldSeparator بین گروه‌ها روی هم بچینید.
  • فرم checkout و ثبت‌نام — تراز label، توضیح و خطا را در input، select و switch یکسان نگه دارید.
  • کارت انتخاب — radio، چک‌باکس یا switch را در کارت‌های قابل‌انتخاب FieldLabel برای انتخاب پلن و پنل ترجیح بپیچید.

نصب

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

کد زیر را کپی کرده و در پروژهٔ خود جای‌گذاری کنید.

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

مسیرهای import را مطابق با پیکربندی پروژهٔ خود به‌روزرسانی کنید.

استفاده

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>

ترکیب

Field

یک کنترل واحد با برچسب، متن راهنما و اعتبارسنجی.

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

FieldGroup

فیلدهای مرتبط در یک گروه. در صورت نیاز از FieldSeparator بین بخش‌ها استفاده کنید.

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

FieldSet

گروه‌بندی معنایی با یک عنوان و توضیحات که معمولاً شامل یک FieldGroup است.

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

آناتومی

خانوادهٔ Field برای ساخت فرم‌های قابل دسترس طراحی شده است. یک فیلد معمولی به این صورت ساختاربندی می‌شود:

<Field>
  <FieldLabel htmlFor="input-id">Label</FieldLabel>
  {/* Input, Select, Switch, etc. */}
  <FieldDescription>Optional helper text.</FieldDescription>
  <FieldError>Validation message.</FieldError>
</Field>
  • Field پوشش اصلی برای یک فیلد واحد است.
  • FieldContent یک ستون flex است که برچسب و توضیحات را گروه‌بندی می‌کند. اگر توضیحاتی ندارید، لازم نیست.
  • فیلدهای مرتبط را با FieldGroup بپوشانید و برای گروه‌بندی معنایی از FieldSet همراه با FieldLegend استفاده کنید.

فرم

برای ساخت فرم‌ها با کامپوننت Field و React Hook Form، Tanstack Form یا Formisch، مستندات Form را ببینید.

نمونه‌ها

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"

کارت انتخاب

کامپوننت‌های Field را در داخل FieldLabel بپوشانید تا گروه‌های فیلد قابل انتخاب بسازید. این با کامپوننت‌های RadioItem، Checkbox و Switch کار می‌کند.

import {
  Field,
  FieldContent,

گروه فیلد

کامپوننت‌های Field را با FieldGroup روی هم بچینید. برای جدا کردن آن‌ها FieldSeparator را اضافه کنید.

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

RTL

برای فعال‌سازی پشتیبانی RTL در Neobrutalism، راهنمای پیکربندی RTL را ببینید.

"use client"

import * as React from "react"

چیدمان واکنش‌گرا

  • فیلدهای عمودی: جهت پیش‌فرض برچسب، کنترل و متن راهنما را روی هم می‌چیند — ایده‌آل برای چیدمان‌های mobile-first.
  • فیلدهای افقی: orientation="horizontal" را روی Field تنظیم کنید تا برچسب و کنترل کنار هم تراز شوند. با FieldContent همراه کنید تا توضیحات تراز باقی بمانند.
  • فیلدهای واکنش‌گرا: برای چیدمان‌های ستونی خودکار در داخل والدهای آگاه از کانتینر، orientation="responsive" را تنظیم کنید. کلاس‌های @container/field-group را روی FieldGroup اعمال کنید تا در نقاط شکست مشخص، جهت را تغییر دهید.
import { Button } from "@/components/ui/button"
import {
  Field,

اعتبارسنجی و خطاها

  • برای تبدیل کل بلوک به حالت خطا، data-invalid را به Field اضافه کنید.
  • برای فناوری‌های کمکی، aria-invalid را روی خود ورودی اضافه کنید.
  • برای هم‌تراز نگه داشتن پیام‌های خطا با فیلد، FieldError را بلافاصله بعد از کنترل یا در داخل FieldContent رندر کنید.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid />
  <FieldError>Enter a valid email address.</FieldError>
</Field>

قابلیت دسترسی

  • FieldSet و FieldLegend کنترل‌های مرتبط را برای کاربران صفحه‌کلید و فناوری کمکی گروه‌بندی‌شده نگه می‌دارند.
  • Field خروجی role="group" می‌دهد، بنابراین کنترل‌های تودرتو هنگام ترکیب، برچسب‌گذاری را از FieldLabel و FieldLegend به ارث می‌برند.
  • FieldSeparator را با احتیاط اعمال کنید تا مطمئن شوید صفحه‌خوان‌ها با مرزهای بخش واضح مواجه می‌شوند.

دسترسی‌پذیری

خانوادهٔ field اول semantics است: FieldSet و FieldLegend مقادیر بومی fieldset/legend را رندر می‌کنند، Field مقدار role="group"، و FieldError مقدار role="alert" تا پیام‌های اعتبارسنجی به‌محض ظاهر شدن اعلام شوند.

  • هر FieldLabel را با کنترلش از طریق htmlFor/id جفت کنید — کلیک روی برچسب کنترل را فوکوس می‌کند و صفحه‌خوان ارتباط را اعلام می‌کند.
  • روی ورودی نامعتبر، aria-invalid را روی خود کنترل بگذارید؛ data-invalid روی Field فقط استایل خطا را می‌راند.
  • رفتار صفحه‌کلید مال کنترل‌های داخل است — wrapperها tab stop یا binding کلید خودشان اضافه نمی‌کنند.

مرجع API

FieldSet

کانتینری که یک fieldset معنایی با پیش‌تنظیم‌های فاصله‌گذاری رندر می‌کند.

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

FieldLegend

عنصر عنوان برای یک FieldSet. برای هم‌تراز شدن با اندازهٔ برچسب به گونهٔ label تغییر دهید.

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

FieldLegend دو گونه دارد: legend و label. گونهٔ label اندازه و تراز برچسب را اعمال می‌کند. اگر FieldSet تودرتو دارید مفید است.

FieldGroup

پوشش چیدمان که کامپوننت‌های Field را روی هم می‌چیند و کوئری‌های کانتینر را برای جهت‌های واکنش‌گرا فعال می‌کند.

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

Field

پوشش اصلی برای یک فیلد واحد. کنترل جهت، استایل‌دهی حالت نامعتبر و فاصله‌گذاری را فراهم می‌کند.

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

FieldContent

ستون flex که کنترل و توضیحات را هنگامی که برچسب کنار کنترل قرار می‌گیرد گروه‌بندی می‌کند. اگر توضیحاتی ندارید، لازم نیست.

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

FieldLabel

برچسبی که هم برای ورودی‌های مستقیم و هم برای فرزندان Field تودرتو استایل‌دهی شده است.

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

FieldTitle

یک عنوان را با استایل برچسب در داخل FieldContent رندر می‌کند.

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

FieldDescription

اسلات متن راهنما که خطوط طولانی را به‌صورت خودکار در چیدمان‌های افقی متعادل می‌کند.

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

FieldSeparator

جداکنندهٔ بصری برای جدا کردن بخش‌ها در داخل یک FieldGroup. محتوای درون‌خطی اختیاری را می‌پذیرد.

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

FieldError

کانتینر خطای قابل دسترس که فرزندان یا یک آرایهٔ errors را می‌پذیرد (برای مثال، از react-hook-form).

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

هنگامی که آرایهٔ errors شامل چند پیام است، کامپوننت به‌صورت خودکار یک فهرست رندر می‌کند.

FieldError همچنین مشکلاتی را که هر اعتبارسنجی که Standard Schema را پیاده‌سازی می‌کند تولید می‌کند می‌پذیرد، از جمله Zod، Valibot و ArkType. آرایهٔ issues را از نتیجهٔ اسکیما مستقیماً منتقل کنید تا یک فهرست خطای یکپارچه در سراسر کتابخانه‌ها رندر شود.