Trường
Các trường dành cho nhãn, trường điều khiển, văn bản hướng dẫn và thông báo lỗi trong các bảng cài đặt, biểu mẫu thanh toán và quy trình đăng ký — với phong cách thiết kế biểu mẫu theo trường phái tân thô mộc.
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {Field gói nhãn, điều khiển, dòng chú thích và thông báo lỗi vào một khối được canh chỉnh gọn gàng, rồi mở rộng lên thành các phần được nhóm lại với FieldGroup và FieldSet. Bên dưới không có primitive nào — chỉ là ngữ nghĩa fieldset, legend và role="group" thuần, kết hợp với Label và Separator, tạo kiểu theo công thức neobrutalist: kiểu chữ tiêu đề đậm cho legend và nhãn, còn viền dày cùng bóng đổ đậm do chính các điều khiển bên trong mang lại.
Nên dùng khi:
- Trang cài đặt — xếp các phần hồ sơ, thông báo và thanh toán bằng legend của
FieldSet, chenFieldSeparatorgiữa các nhóm. - Biểu mẫu thanh toán và đăng ký — giữ nhãn, mô tả và lỗi canh đều nhau trên các input, select và switch.
- Thẻ lựa chọn — bọc radio, checkbox hoặc switch trong các thẻ
FieldLabelchọn được, dùng cho bảng chọn gói và bảng tùy chọn.
Cài đặt#
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
Sao chép và dán đoạn mã sau vào dự án của bạn.
"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,
}
Cập nhật các đường dẫn import cho phù hợp với thiết lập dự án của bạn.
Cách dùng#
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>Kết hợp#
Field#
Một điều khiển duy nhất với nhãn, văn bản trợ giúp và xác thực.
Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldErrorFieldGroup#
Các trường liên quan trong một nhóm. Dùng FieldSeparator giữa các phần khi cần.
FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
├── FieldSeparator
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectFieldSet#
Nhóm ngữ nghĩa với một chú thích và mô tả, thường chứa một FieldGroup.
FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
├── Field
│ ├── FieldLabel
│ ├── Input / Textarea / Switch / Select
│ ├── FieldDescription
│ └── FieldError
└── Field
├── FieldLabel
└── Input / Textarea / Switch / SelectGiải phẫu#
Nhóm Field được thiết kế để tạo các form dễ truy cập. Một trường điển hình được cấu trúc như sau:
<Field>
<FieldLabel htmlFor="input-id">Label</FieldLabel>
{/* Input, Select, Switch, etc. */}
<FieldDescription>Optional helper text.</FieldDescription>
<FieldError>Validation message.</FieldError>
</Field>Fieldlà wrapper cốt lõi cho một trường duy nhất.FieldContentlà một cột flex nhóm nhãn và mô tả. Không bắt buộc nếu bạn không có mô tả.- Bao bọc các trường liên quan bằng
FieldGroup, và dùngFieldSetvớiFieldLegendđể nhóm ngữ nghĩa.
Form#
Xem tài liệu Form để xây dựng form với thành phần Field và React Hook Form, Tanstack Form, hoặc Formisch.
Ví dụ#
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"
Thẻ lựa chọn#
Bao bọc các thành phần Field bên trong FieldLabel để tạo các nhóm trường có thể chọn. Cách này hoạt động với các thành phần RadioItem, Checkbox và Switch.
import {
Field,
FieldContent,Nhóm trường#
Xếp chồng các thành phần Field bằng FieldGroup. Thêm FieldSeparator để chia chúng.
import { Checkbox } from "@/components/ui/checkbox"
import {
Field,RTL#
Để bật hỗ trợ RTL trong Neobrutalism, hãy xem hướng dẫn cấu hình RTL.
"use client"
import * as React from "react"Bố cục đáp ứng#
- Trường dọc: Hướng mặc định xếp chồng nhãn, điều khiển và văn bản trợ giúp — lý tưởng cho các bố cục ưu tiên di động.
- Trường ngang: Đặt
orientation="horizontal"trênFieldđể căn nhãn và điều khiển cạnh nhau. Kết hợp vớiFieldContentđể giữ các mô tả được căn chỉnh. - Trường đáp ứng: Đặt
orientation="responsive"để có các bố cục cột tự động bên trong các phần tử cha nhận biết container. Áp dụng các lớp@container/field-grouptrênFieldGroupđể chuyển đổi hướng tại các breakpoint cụ thể.
import { Button } from "@/components/ui/button"
import {
Field,Xác thực và lỗi#
- Thêm
data-invalidvàoFieldđể chuyển toàn bộ khối sang trạng thái lỗi. - Thêm
aria-invalidtrên chính ô nhập cho các công nghệ hỗ trợ. - Render
FieldErrorngay sau điều khiển hoặc bên trongFieldContentđể giữ các thông báo lỗi được căn chỉnh với trường.
<Field data-invalid>
<FieldLabel htmlFor="email">Email</FieldLabel>
<Input id="email" type="email" aria-invalid />
<FieldError>Enter a valid email address.</FieldError>
</Field>Khả năng truy cập#
FieldSetvàFieldLegendgiữ các điều khiển liên quan được nhóm lại cho người dùng bàn phím và công nghệ hỗ trợ.Fieldxuất rarole="group"để các điều khiển lồng nhau kế thừa việc gắn nhãn từFieldLabelvàFieldLegendkhi kết hợp.- Áp dụng
FieldSeparatormột cách hạn chế để đảm bảo trình đọc màn hình gặp các ranh giới phần rõ ràng.
Khả năng truy cập#
Bộ Field đặt ngữ nghĩa lên trước: FieldSet và FieldLegend render fieldset/legend native, Field render role="group", còn FieldError render role="alert" để các thông báo kiểm tra hợp lệ được đọc lên ngay khi xuất hiện.
- Luôn ghép mỗi
FieldLabelvới điều khiển của nó quahtmlFor/id— bấm vào nhãn sẽ focus vào điều khiển và trình đọc màn hình đọc lên đúng mối liên hệ đó. - Khi dữ liệu không hợp lệ, hãy đặt
aria-invalidlên chính điều khiển;data-invalidtrênFieldchỉ để tạo kiểu cho trạng thái lỗi. - Hành vi bàn phím thuộc về các điều khiển bên trong — các lớp bọc không thêm điểm dừng focus hay tổ hợp phím nào của riêng chúng.
Tham chiếu API#
FieldSet#
Container render một fieldset ngữ nghĩa với các thiết lập khoảng cách sẵn.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSet>
<FieldLegend>Delivery</FieldLegend>
<FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>FieldLegend#
Phần tử chú thích cho một FieldSet. Chuyển sang biến thể label để căn với kích thước nhãn.
| Prop | Type | Default |
|---|---|---|
variant | "legend" | "label" | "legend" |
className | string |
<FieldLegend variant="label">Notification Preferences</FieldLegend>FieldLegend có hai biến thể: legend và label. Biến thể label áp dụng kích thước và căn chỉnh của nhãn. Hữu ích nếu bạn có các FieldSet lồng nhau.
FieldGroup#
Wrapper bố cục xếp chồng các thành phần Field và bật container query cho các hướng đáp ứng.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldGroup className="@container/field-group flex flex-col gap-6">
<Field>{/* ... */}</Field>
<Field>{/* ... */}</Field>
</FieldGroup>Field#
Wrapper cốt lõi cho một trường duy nhất. Cung cấp kiểm soát hướng, tạo kiểu trạng thái không hợp lệ và khoảng cách.
| 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#
Cột flex nhóm điều khiển và các mô tả khi nhãn nằm bên cạnh điều khiển. Không bắt buộc nếu bạn không có mô tả.
| 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#
Nhãn được tạo kiểu cho cả ô nhập trực tiếp và các phần tử con Field lồng nhau.
| Prop | Type | Default |
|---|---|---|
className | string | |
asChild | boolean | false |
<FieldLabel htmlFor="email">Email</FieldLabel>FieldTitle#
Render một tiêu đề với kiểu nhãn bên trong FieldContent.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldContent>
<FieldTitle>Enable Touch ID</FieldTitle>
<FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>FieldDescription#
Slot văn bản trợ giúp tự động cân bằng các dòng dài trong các bố cục ngang.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldDescription>We never share your email with anyone.</FieldDescription>FieldSeparator#
Dấu phân chia trực quan để phân tách các phần bên trong một FieldGroup. Chấp nhận nội dung nội tuyến tùy chọn.
| Prop | Type | Default |
|---|---|---|
className | string |
<FieldSeparator>Or continue with</FieldSeparator>FieldError#
Container lỗi dễ truy cập chấp nhận các phần tử con hoặc một mảng errors (ví dụ: từ react-hook-form).
| Prop | Type | Default |
|---|---|---|
errors | Array<{ message?: string } | undefined> | |
className | string |
<FieldError errors={errors.username} />Khi mảng errors chứa nhiều thông báo, thành phần tự động render một danh sách.
FieldError cũng chấp nhận các vấn đề được tạo ra bởi bất kỳ trình xác thực nào triển khai Standard Schema, bao gồm Zod, Valibot và ArkType. Truyền trực tiếp mảng issues từ kết quả schema để render một danh sách lỗi thống nhất trên các thư viện.