İçeriğe atla

Alan

Ayar panelleri, ödeme formları ve kayıt akışları için etiket, kontrol, yardım metni ve hata alanları — neobrütalist form tasarımıyla.

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

Field bileşeni; etiketi, kontrolü, yardım metnini ve hata mesajını hizalı tek bir blokta toplar ve FieldGroup ile FieldSet sayesinde gruplanmış bölümlere ölçeklenir. Altta bir primitif yoktur — Label ve Separator ile birleştirilmiş düz fieldset, legend ve role="group" semantiği, neobrütalist reçeteye göre stillendirilmiştir: legend ve etiketlerde vurgulu başlık tipografisi; kalın kenarlıkları ve keskin gölgeleri ise sarmalanan kontroller getirir.

Şu durumlarda tercih edin:

  • Ayar sayfaları — profil, bildirim ve faturalama bölümlerini FieldSet legend'larıyla üst üste dizin, grupların arasına FieldSeparator koyun.
  • Ödeme ve kayıt formları — etiket, açıklama ve hata hizalamasını giriş alanları, seçim kutuları ve anahtarlar arasında tutarlı tutun.
  • Seçim kartları — plan seçiciler ve tercih panelleri için radyo düğmelerini, onay kutularını veya anahtarları seçilebilir FieldLabel kartlarıyla sarın.

Kurulum

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

Aşağıdaki kodu kopyalayıp projenize yapıştırın.

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

İçe aktarma yollarını proje kurulumunuza uyacak şekilde güncelleyin.

Kullanım

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>

Kompozisyon

Field

Etiket, yardımcı metin ve doğrulama içeren tek bir kontrol.

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

FieldGroup

Tek bir grupta ilgili alanlar. Gerektiğinde bölümler arasında FieldSeparator kullanın.

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

FieldSet

Genellikle bir FieldGroup içeren, bir başlık ve açıklama içeren anlamsal gruplama.

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

Anatomi

Field ailesi, erişilebilir formlar oluşturmak için tasarlanmıştır. Tipik bir alan aşağıdaki gibi yapılandırılır:

<Field>
  <FieldLabel htmlFor="input-id">Label</FieldLabel>
  {/* Input, Select, Switch, etc. */}
  <FieldDescription>Optional helper text.</FieldDescription>
  <FieldError>Validation message.</FieldError>
</Field>
  • Field, tek bir alan için temel sarmalayıcıdır.
  • FieldContent, etiket ve açıklamayı gruplayan bir flex sütunudur. Açıklamanız yoksa gerekli değildir.
  • İlgili alanları FieldGroup ile sarmalayın ve anlamsal gruplama için FieldSet'i FieldLegend ile kullanın.

Form

Field bileşeni ve React Hook Form, Tanstack Form veya Formisch ile form oluşturmak için Form belgelerine bakın.

Örnekler

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"

Seçim Kartı

Seçilebilir alan grupları oluşturmak için Field bileşenlerini FieldLabel içine sarmalayın. Bu, RadioItem, Checkbox ve Switch bileşenleriyle çalışır.

import {
  Field,
  FieldContent,

Alan Grubu

Field bileşenlerini FieldGroup ile üst üste yerleştirin. Bunları ayırmak için FieldSeparator ekleyin.

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

RTL

Neobrutalism'de RTL desteğini etkinleştirmek için RTL yapılandırma kılavuzuna bakın.

"use client"

import * as React from "react"

Duyarlı Düzen

  • Dikey alanlar: Varsayılan yönlendirme etiketi, kontrolü ve yardımcı metni üst üste yerleştirir — mobil öncelikli düzenler için idealdir.
  • Yatay alanlar: Etiketi ve kontrolü yan yana hizalamak için Field üzerinde orientation="horizontal" ayarlayın. Açıklamaları hizalı tutmak için FieldContent ile birlikte kullanın.
  • Duyarlı alanlar: Konteyner farkındalıklı üst öğeler içinde otomatik sütun düzenleri için orientation="responsive" ayarlayın. Belirli kesme noktalarında yönlendirmeleri değiştirmek için FieldGroup üzerinde @container/field-group sınıflarını uygulayın.
import { Button } from "@/components/ui/button"
import {
  Field,

Doğrulama ve Hatalar

  • Tüm bloğu bir hata durumuna geçirmek için Field'e data-invalid ekleyin.
  • Yardımcı teknolojiler için girişin kendisine aria-invalid ekleyin.
  • Hata mesajlarını alanla hizalı tutmak için FieldError'ı kontrolden hemen sonra veya FieldContent içinde render edin.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid />
  <FieldError>Enter a valid email address.</FieldError>
</Field>

Erişilebilirlik

Field ailesi semantiği önceler: FieldSet ve FieldLegend yerel fieldset/legend öğelerini işler, Field role="group" olarak, FieldError ise role="alert" olarak işlenir; böylece doğrulama mesajları göründükleri anda duyurulur.

  • Her FieldLabel bileşenini htmlFor/id ile kendi kontrolüne bağlayın — etikete tıklamak kontrolü odaklar ve ekran okuyucular bu ilişkiyi duyurur.
  • Geçersiz girişte aria-invalid özniteliğini kontrolün kendisine ekleyin; Field üzerindeki data-invalid yalnızca hata stilini yönetir.
  • Klavye davranışı içerideki kontrollere aittir — sarmalayıcılar kendilerine ait odak durağı veya tuş ataması eklemez.

API Referansı

FieldSet

Boşluk ön ayarlarıyla anlamsal bir fieldset render eden konteyner.

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

FieldLegend

Bir FieldSet için başlık öğesi. Etiket boyutlandırmasıyla hizalamak için label varyantına geçin.

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

FieldLegend'in iki varyantı vardır: legend ve label. label varyantı, etiket boyutlandırması ve hizalamasını uygular. İç içe FieldSet varsa kullanışlıdır.

FieldGroup

Field bileşenlerini üst üste yerleştiren ve duyarlı yönlendirmeler için konteyner sorgularını etkinleştiren düzen sarmalayıcısı.

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

Field

Tek bir alan için temel sarmalayıcı. Yönlendirme kontrolü, geçersiz durum stili ve boşluk sağlar.

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

FieldContent

Etiket kontrolün yanında olduğunda kontrolü ve açıklamaları gruplayan flex sütunu. Açıklamanız yoksa gerekli değildir.

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

FieldLabel

Hem doğrudan girişler hem de iç içe Field alt öğeleri için stillendirilmiş etiket.

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

FieldTitle

FieldContent içinde etiket stiliyle bir başlık render eder.

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

FieldDescription

Yatay düzenlerde uzun satırları otomatik olarak dengeleyen yardımcı metin yuvası.

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

FieldSeparator

Bir FieldGroup içindeki bölümleri ayırmak için görsel ayırıcı. İsteğe bağlı satır içi içerik kabul eder.

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

FieldError

Alt öğeleri veya bir errors dizisini (örneğin react-hook-form'dan) kabul eden erişilebilir hata konteyneri.

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

errors dizisi birden fazla mesaj içerdiğinde, bileşen otomatik olarak bir liste render eder.

FieldError ayrıca Zod, Valibot ve ArkType dahil olmak üzere Standard Schema'yı uygulayan herhangi bir doğrulayıcı tarafından üretilen sorunları da kabul eder. Kitaplıklar arasında birleşik bir hata listesi render etmek için şema sonucundaki issues dizisini doğrudan iletin.