Combine labels, controls, and help text to compose accessible form fields and grouped inputs.
Installation
pnpm dlx sprawlify@latest add fieldnpx sprawlify@latest add fieldyarn sprawlify@latest add fieldbunx --bun sprawlify@latest add field
Install the following dependencies:
pnpm add @sprawlify/primitives @sprawlify/reactnpm install @sprawlify/primitives @sprawlify/reactyarn add @sprawlify/primitives @sprawlify/reactbun add @sprawlify/primitives @sprawlify/react
Add the following files to your project:
1"use client";23import * as React from "react";4import { useMemo } from "react";5import { cn } from "@/lib/utils";6import { Separator } from "@/components/ui/separator";7import { sprawlify } from "@sprawlify/react";8import { Field as FieldPrimitive } from "@sprawlify/react/field";9import { Fieldset as FieldsetPrimitive } from "@sprawlify/react/fieldset";10import { cva, type VariantProps } from "class-variance-authority";1112function FieldSet({ className, ...props }: React.ComponentProps<typeof FieldsetPrimitive.Root>) {13 return (14 <FieldsetPrimitive.Root15 data-slot="field-set"16 className={cn(17 "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",18 className,19 )}20 {...props}21 />22 );23}2425function FieldLegend({26 className,27 variant = "legend",28 ...props29}: React.ComponentProps<typeof FieldsetPrimitive.Legend> & {30 variant?: "legend" | "label";31}) {32 return (33 <FieldsetPrimitive.Legend34 data-slot="field-legend"35 data-variant={variant}36 className={cn(37 "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",38 className,39 )}40 {...props}41 />42 );43}4445function FieldGroup({ className, ...props }: React.ComponentProps<typeof sprawlify.div>) {46 return (47 <sprawlify.div48 data-slot="field-group"49 className={cn(50 "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",51 className,52 )}53 {...props}54 />55 );56}5758const fieldVariants = cva("group/field flex w-full gap-2 data-[invalid=true]:text-destructive", {59 variants: {60 orientation: {61 vertical: "flex-col *:w-full [&>.sr-only]:w-auto",62 horizontal:63 "has-[>[data-slot=field-content]]:&>[role=checkbox],[role=radio]]:mt-px flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto",64 responsive:65 "@md/field-group:has-[>[data-slot=field-content]]:&>[role=checkbox],[role=radio]]:mt-px 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",66 },67 },68 defaultVariants: {69 orientation: "vertical",70 },71});7273function Field({74 className,75 orientation = "vertical",76 ...props77}: React.ComponentProps<typeof FieldPrimitive.Root> & VariantProps<typeof fieldVariants>) {78 return (79 <FieldPrimitive.Root80 data-slot="field"81 data-orientation={orientation}82 className={cn(fieldVariants({ orientation }), className)}83 {...props}84 />85 );86}8788function FieldContent({ className, ...props }: React.ComponentProps<typeof sprawlify.div>) {89 return (90 <sprawlify.div91 data-scope="field"92 data-part="content"93 data-slot="field-content"94 className={cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", className)}95 {...props}96 />97 );98}99100function FieldLabel({ className, ...props }: React.ComponentProps<typeof FieldPrimitive.Label>) {101 return (102 <FieldPrimitive.Label103 data-slot="field-label"104 className={cn(105 "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-[state=checked]:border-primary/30 has-data-[state=checked]:bg-primary/5 has-[>[data-slot=field]]:rounded-lg has-[>[data-slot=field]]:border *:data-[slot=field]:p-2.5 dark:has-data-[state=checked]:border-primary/20 dark:has-data-[state=checked]:bg-primary/10",106 "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",107 className,108 )}109 {...props}110 />111 );112}113114function FieldTitle({ className, ...props }: React.ComponentProps<typeof sprawlify.div>) {115 return (116 <sprawlify.div117 data-scope="field"118 data-part="title"119 data-slot="field-title"120 className={cn(121 "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",122 className,123 )}124 {...props}125 />126 );127}128129function FieldDescription({130 className,131 ...props132}: React.ComponentProps<typeof FieldPrimitive.HelperText>) {133 return (134 <FieldPrimitive.HelperText135 data-slot="field-description"136 className={cn(137 "text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",138 "last:mt-0 nth-last-2:-mt-1",139 "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",140 className,141 )}142 {...props}143 />144 );145}146147function FieldSeparator({148 children,149 className,150 ...props151}: React.ComponentProps<typeof sprawlify.div> & {152 children?: React.ReactNode;153}) {154 return (155 <sprawlify.div156 data-scope="field"157 data-part="separator"158 data-slot="field-separator"159 data-content={!!children}160 className={cn(161 "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",162 className,163 )}164 {...props}165 >166 <Separator className="absolute inset-0 top-1/2" />167 {children && (168 <sprawlify.span169 data-scope="field"170 data-part="separator-content"171 className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"172 data-slot="field-separator-content"173 >174 {children}175 </sprawlify.span>176 )}177 </sprawlify.div>178 );179}180181function FieldError({182 className,183 children,184 errors,185 ...props186}: React.ComponentProps<typeof FieldPrimitive.ErrorText> & {187 errors?: Array<{ message?: string } | undefined>;188}) {189 const content = useMemo(() => {190 if (children) {191 return children;192 }193194 if (!errors?.length) {195 return null;196 }197198 const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];199200 if (uniqueErrors?.length == 1) {201 return uniqueErrors[0]?.message;202 }203204 return (205 <sprawlify.ul className="ml-4 flex list-disc flex-col gap-1">206 {uniqueErrors.map(207 (error, index) =>208 error?.message && <sprawlify.li key={index}>{error.message}</sprawlify.li>,209 )}210 </sprawlify.ul>211 );212 }, [children, errors]);213214 if (!content) {215 return null;216 }217218 return (219 <FieldPrimitive.ErrorText220 role="alert"221 data-scope="field"222 data-part="error"223 data-slot="field-error"224 className={cn("text-sm font-normal text-destructive", className)}225 {...props}226 >227 {content}228 </FieldPrimitive.ErrorText>229 );230}231232function FieldRequiredIndicator({233 className,234 ...props235}: React.ComponentProps<typeof FieldPrimitive.RequiredIndicator>) {236 return (237 <FieldPrimitive.RequiredIndicator238 data-slot="field-required-indicator"239 className={cn("text-destructive", className)}240 {...props}241 />242 );243}244245export {246 Field,247 FieldLabel,248 FieldDescription,249 FieldError,250 FieldGroup,251 FieldLegend,252 FieldSeparator,253 FieldSet,254 FieldContent,255 FieldTitle,256 FieldRequiredIndicator,257};258Update the import paths to match your project setup.
Usage
1import {
2 FieldSet,
3 FieldLegend,
4 FieldGroup,
5 Field,
6 FieldContent,
7 FieldLabel,
8 FieldTitle,
9 FieldDescription,
10 FieldSeparator,
11 FieldError
12} from "@/components/ui/field"1<FieldSet>
2 <FieldLegend>Profile</FieldLegend>
3 <FieldDescription>This appears on invoices and emails.</FieldDescription>
4 <FieldGroup>
5 <Field>
6 <FieldLabel htmlFor="name">Full name</FieldLabel>
7 <Input id="name" autoComplete="off" placeholder="Evil Rabbit" />
8 <FieldDescription>This appears on invoices and emails.</FieldDescription>
9 </Field>
10 <Field>
11 <FieldLabel htmlFor="username">Username</FieldLabel>
12 <Input id="username" autoComplete="off" aria-invalid />
13 <FieldError>Choose another username.</FieldError>
14 </Field>
15 <Field orientation="horizontal">
16 <Switch id="newsletter" />
17 <FieldLabel htmlFor="newsletter">Subscribe to the newsletter</FieldLabel>
18 </Field>
19 </FieldGroup>
20</FieldSet>Examples
Checkbox
1import { Checkbox } from "@/components/ui/checkbox";2import {3 Field,4 FieldContent,Field Group
1import { Checkbox } from "@/components/ui/checkbox";2import {3 Field,4 FieldDescription,Fieldset
1import {2 Field,3 FieldDescription,4 FieldGroup,Input
1import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from "@/components/ui/field";2import { Input } from "@/components/ui/input";34export default function Preview() {Textarea
1import { Field, FieldDescription, FieldGroup, FieldLabel, FieldSet } from "@/components/ui/field";2import { Textarea } from "@/components/ui/textarea";34export default function Preview() {Get PRO
Need premium blocks and templates? Upgrade to PRO and get access.