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/solidnpm install @sprawlify/primitives @sprawlify/solidyarn add @sprawlify/primitives @sprawlify/solidbun add @sprawlify/primitives @sprawlify/solid
Add the following files to your project:
1import { createMemo, type ComponentProps, For } from "solid-js";2import { splitProps, Show } from "solid-js";3import { cn } from "@/lib/utils";4import { Separator } from "@/components/ui/separator";5import { sprawlify } from "@sprawlify/solid";6import { Field as FieldPrimitive } from "@sprawlify/solid/field";7import { Fieldset as FieldsetPrimitive } from "@sprawlify/solid/fieldset";8import { cva, type VariantProps } from "class-variance-authority";910function FieldSet(props: ComponentProps<typeof FieldsetPrimitive.Root>) {11 const [local, others] = splitProps(props, ["class"]);1213 return (14 <FieldsetPrimitive.Root15 data-slot="field-set"16 class={cn(17 "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",18 local.class,19 )}20 {...others}21 />22 );23}2425function FieldLegend(26 props: ComponentProps<typeof FieldsetPrimitive.Legend> & {27 variant?: "legend" | "label";28 },29) {30 const [local, others] = splitProps(props, ["class", "variant"]);31 const variant = () => local.variant ?? "legend";3233 return (34 <FieldsetPrimitive.Legend35 data-slot="field-legend"36 data-variant={variant()}37 class={cn(38 "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",39 local.class,40 )}41 {...others}42 />43 );44}4546function FieldGroup(props: ComponentProps<typeof sprawlify.div>) {47 const [local, others] = splitProps(props, ["class"]);4849 return (50 <sprawlify.div51 data-scope="field"52 data-part="group"53 data-slot="field-group"54 class={cn(55 "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",56 local.class,57 )}58 {...others}59 />60 );61}6263const fieldVariants = cva("group/field flex w-full gap-2 data-[invalid=true]:text-destructive", {64 variants: {65 orientation: {66 vertical: "flex-col *:w-full [&>.sr-only]:w-auto",67 horizontal:68 "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",69 responsive:70 "@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",71 },72 },73 defaultVariants: {74 orientation: "vertical",75 },76});7778function Field(79 props: ComponentProps<typeof FieldPrimitive.Root> & VariantProps<typeof fieldVariants>,80) {81 const [local, others] = splitProps(props, ["class", "orientation"]);82 const orientation = () => local.orientation ?? "vertical";8384 return (85 <FieldPrimitive.Root86 data-slot="field"87 data-orientation={orientation()}88 class={cn(fieldVariants({ orientation: orientation() }), local.class)}89 {...others}90 />91 );92}9394function FieldContent(props: ComponentProps<typeof sprawlify.div>) {95 const [local, others] = splitProps(props, ["class"]);9697 return (98 <sprawlify.div99 data-scope="field"100 data-part="content"101 data-slot="field-content"102 class={cn("group/field-content flex flex-1 flex-col gap-0.5 leading-snug", local.class)}103 {...others}104 />105 );106}107108function FieldLabel(props: ComponentProps<typeof FieldPrimitive.Label>) {109 const [local, others] = splitProps(props, ["class"]);110111 return (112 <FieldPrimitive.Label113 data-slot="field-label"114 class={cn(115 "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",116 "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",117 local.class,118 )}119 {...others}120 />121 );122}123124function FieldTitle(props: ComponentProps<typeof sprawlify.div>) {125 const [local, others] = splitProps(props, ["class"]);126127 return (128 <sprawlify.div129 data-scope="field"130 data-part="title"131 data-slot="field-title"132 class={cn(133 "flex w-fit items-center gap-2 text-sm leading-snug font-medium group-data-[disabled=true]/field:opacity-50",134 local.class,135 )}136 {...others}137 />138 );139}140141function FieldDescription(props: ComponentProps<typeof FieldPrimitive.HelperText>) {142 const [local, others] = splitProps(props, ["class"]);143144 return (145 <FieldPrimitive.HelperText146 data-slot="field-description"147 class={cn(148 "text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",149 "last:mt-0 nth-last-2:-mt-1",150 "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",151 local.class,152 )}153 {...others}154 />155 );156}157158function FieldSeparator(props: ComponentProps<typeof sprawlify.div> & { children?: any }) {159 const [local, others] = splitProps(props, ["children", "class"]);160161 return (162 <sprawlify.div163 data-scope="field"164 data-part="separator"165 data-slot="field-separator"166 data-content={!!local.children}167 class={cn(168 "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",169 local.class,170 )}171 {...others}172 >173 <Separator class="absolute inset-0 top-1/2" />174 <Show when={local.children}>175 <sprawlify.span176 class="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"177 data-slot="field-separator-content"178 >179 {local.children}180 </sprawlify.span>181 </Show>182 </sprawlify.div>183 );184}185186function FieldError(187 props: ComponentProps<typeof FieldPrimitive.ErrorText> & {188 errors?: Array<{ message?: string } | undefined>;189 },190) {191 const [local, others] = splitProps(props, ["children", "errors", "class"]);192193 const content = createMemo(() => {194 if (local.children) {195 return local.children;196 }197198 if (!local.errors?.length) {199 return null;200 }201202 const uniqueErrors = [203 ...new Map(local.errors.map((error) => [error?.message, error])).values(),204 ];205206 if (uniqueErrors?.length === 1) {207 return uniqueErrors[0]?.message;208 }209210 return (211 <sprawlify.ul class="ml-4 flex list-disc flex-col gap-1">212 <For each={uniqueErrors}>213 {(error) => error?.message && <sprawlify.li>{error.message}</sprawlify.li>}214 </For>215 </sprawlify.ul>216 );217 });218219 return (220 <Show when={content()}>221 <FieldPrimitive.ErrorText222 data-slot="field-error"223 class={cn("text-sm font-normal text-destructive", local.class)}224 {...others}225 >226 {content()}227 </FieldPrimitive.ErrorText>228 </Show>229 );230}231232function FieldRequiredIndicator(props: ComponentProps<typeof FieldPrimitive.RequiredIndicator>) {233 const [local, others] = splitProps(props, ["class"]);234235 return (236 <FieldPrimitive.RequiredIndicator237 data-slot="field-required-indicator"238 class={cn("text-destructive", local.class)}239 {...others}240 />241 );242}243244export {245 Field,246 FieldLabel,247 FieldDescription,248 FieldError,249 FieldGroup,250 FieldLegend,251 FieldSeparator,252 FieldSet,253 FieldContent,254 FieldTitle,255 FieldRequiredIndicator,256};257Update 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 for="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 for="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 for="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 { Field, FieldDescription, FieldGroup, FieldLabel } from "@/components/ui/field";2import { Input } from "@/components/ui/input";34export default function Preview() {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 } from "@/components/ui/field";23export default function Preview() {4 return (Get PRO
Need premium blocks and templates? Upgrade to PRO and get access.