Field
Layout wrapper pairing a label, control, description, and error message. Install Field from the VLLNT UI registry with the shadcn CLI.
Layout wrapper pairing a label, control, description, and error message. Part of the form family in VLLNT UI, it ships as a machine-readable registry entry — copy the source directly into your app with the shadcn CLI and own it, no runtime dependency on a component library.
Preview
Switch between light and dark to inspect the embedded Storybook preview.
Installation
Add Field to your project with the shadcn CLI. The source lands in your codebase, ready to adapt:
pnpm dlx shadcn@latest add https://ui.vllnt.ai/r/field.jsonSource
"use client";
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "../../lib/utils";
import { Label } from "../label/label";
type FieldContextValue = {
descriptionId: string;
errorId: string;
id: string;
invalid: boolean;
};
const FieldContext = React.createContext<FieldContextValue | null>(null);
function useFieldContext(): FieldContextValue {
const context = React.use(FieldContext);
if (!context) {
throw new Error("Field subcomponents must be used within a Field");
}
return context;
}
const fieldVariants = cva("flex", {
defaultVariants: {
orientation: "vertical",
},
variants: {
orientation: {
horizontal: "flex-row items-center gap-3",
vertical: "flex-col gap-1.5",
},
},
});
/** Groups a label, control, description, and error message for one input. */
export type FieldProps = {
children: React.ReactNode;
className?: string;
invalid?: boolean;
} & VariantProps<typeof fieldVariants>;
const Field = ({
children,
className,
invalid = false,
orientation,
ref,
}: FieldProps & { ref?: React.Ref<HTMLDivElement> }) => {
const id = React.useId();
const value = React.useMemo<FieldContextValue>(
() => ({
descriptionId: `${id}-description`,
errorId: `${id}-error`,
id: `${id}-control`,
invalid,
}),
[id, invalid],
);
return (
<FieldContext.Provider value={value}>
<div
className={cn(fieldVariants({ orientation }), className)}
data-slot="field"
ref={ref}
>
{children}
</div>
</FieldContext.Provider>
);
};
Field.displayName = "Field";
/** Label wired to the field control through htmlFor. */
export type FieldLabelProps = React.ComponentPropsWithoutRef<typeof Label>;
const FieldLabel = ({
className,
ref,
...props
}: FieldLabelProps & { ref?: React.Ref<React.ComponentRef<typeof Label>> }) => {
const { id, invalid } = useFieldContext();
return (
<Label
className={cn(invalid && "text-destructive", className)}
htmlFor={id}
ref={ref}
{...props}
/>
);
};
FieldLabel.displayName = "FieldLabel";
/** Wraps the control and injects id plus aria wiring from the Field. */
export type FieldControlProps = {
children: React.ReactNode;
};
const FieldControl = ({
children,
ref,
}: FieldControlProps & { ref?: React.Ref<HTMLElement> }) => {
const { descriptionId, errorId, id, invalid } = useFieldContext();
const describedBy =
[descriptionId, invalid ? errorId : null].filter(Boolean).join(" ") ||
undefined;
return (
<Slot
aria-describedby={describedBy}
aria-invalid={invalid || undefined}
id={id}
ref={ref}
>
{children}
</Slot>
);
};
FieldControl.displayName = "FieldControl";
/** Helper text describing the field, announced to assistive tech. */
export type FieldDescriptionProps = React.ComponentPropsWithoutRef<"p">;
const FieldDescription = ({
className,
ref,
...props
}: FieldDescriptionProps & { ref?: React.Ref<HTMLParagraphElement> }) => {
const { descriptionId } = useFieldContext();
return (
<p
className={cn("text-sm text-muted-foreground", className)}
id={descriptionId}
ref={ref}
{...props}
/>
);
};
FieldDescription.displayName = "FieldDescription";
/** Validation message that renders when children are present. */
export type FieldErrorProps = React.ComponentPropsWithoutRef<"p">;
const FieldError = ({
children,
className,
ref,
...props
}: FieldErrorProps & { ref?: React.Ref<HTMLParagraphElement> }) => {
const { errorId } = useFieldContext();
if (!children) {
return null;
}
return (
<p
className={cn("text-sm font-medium text-destructive", className)}
id={errorId}
ref={ref}
role="alert"
{...props}
>
{children}
</p>
);
};
FieldError.displayName = "FieldError";
export {
Field,
FieldControl,
FieldDescription,
FieldError,
FieldLabel,
fieldVariants,
};
Stories
Explore every variant and state in the interactive Storybook:
Preview
Switch between light and dark to inspect the embedded Storybook preview.