Docs
Form Components

Form Components

Base components for building accessible form components with React-Aria-Components.

Installation

Copy and paste the following code into your project.

"use client";
 
import { Slot } from "@radix-ui/react-slot";
import type { VariantProps } from "cva";
import { cva } from "cva";
import type { TextFieldProps } from "react-aria-components";
import {
  Input as RaInput,
  Label as RaLabel,
  TextArea as RaTextArea,
  Text,
} from "react-aria-components";
 
import { autoRef, cn, withRenderProps } from "@/lib/utils";
 
/* -------------------------------------------------------------------------- */
/*                                   Helpers                                  */
/* -------------------------------------------------------------------------- */
 
export interface FormComponentBaseProps {
  label?: string;
  description?: string;
  errorMessage?: string;
  onPressEnter?: () => void;
}
 
export const handleOnKeyDown =
  (
    onKeyDown?: TextFieldProps["onKeyDown"],
    onPressEnter?: FormComponentBaseProps["onPressEnter"],
  ) =>
  (e: Parameters<NonNullable<TextFieldProps["onKeyDown"]>>[0]) => {
    if (onPressEnter && e.key === "Enter") {
      onPressEnter();
    }
 
    onKeyDown?.(e);
  };
 
/* -------------------------------------------------------------------------- */
/*                                  Variants                                  */
/* -------------------------------------------------------------------------- */
 
export const formVariants = {
  wrapper: cva({
    base: "flex flex-col space-y-2",
  }),
  label: cva({
    base: "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
  }),
  description: cva({
    base: "text-sm text-muted-foreground",
  }),
  error: cva({
    base: "text-sm font-medium text-destructive",
  }),
  input: {
    base: cva({
      base: cn(
        "flex w-full rounded-md border border-input  bg-background px-3 py-2 text-base sm:text-sm text-foreground",
        "file:border-0 file:bg-transparent file:text-sm file:font-medium",
        "placeholder:text-sm placeholder:text-muted-foreground",
        "focus-visible:outline-none focus-visible:ring-0",
        "disabled:cursor-not-allowed disabled:opacity-50",
      ),
      variants: {
        ring: {
          true: cn(
            "focus-visible:ring-primary focus-visible:ring-1 focus-visible:border-primary",
          ),
          false: "",
        },
        multiline: {
          true: "",
          false: "h-10",
        },
      },
      defaultVariants: {
        ring: true,
        multiline: false,
      },
    }),
    ring: cva({
      base: cn(
        "rounded-md",
        "focus-visible:ring-primary focus-visible:ring-1 focus-visible:border-primary",
        "s-focus-within:ring-primary s-focus-within:ring-1 s-focus-within:border-primary",
        /*         "data-[focused=true]:ring-2 data-[focused=true]:ring-offset-2",
        "ring-ring data-[focus-within=true]:ring-2 data-[focus-within=true]:ring-offset-2", */
      ),
    }),
  },
};
 
/* -------------------------------------------------------------------------- */
/*                                 Components                                 */
/* -------------------------------------------------------------------------- */
 
/* --------------------------------- Wrapper -------------------------------- */
 
export type FormCompWrapperProps = React.ComponentPropsWithRef<"div">;
 
export const FormCompWrapper = autoRef(
  ({ className, ...props }: FormCompWrapperProps) => {
    return <div className={cn(formVariants.wrapper(), className)} {...props} />;
  },
);
 
/* ---------------------------------- Input --------------------------------- */
 
export type InputProps = React.ComponentPropsWithRef<typeof RaInput> &
  Omit<VariantProps<typeof formVariants.input.base>, "multiline">;
 
export const Input = autoRef(({ className, ring, ...props }: InputProps) => {
  return (
    <RaInput
      className={(values) =>
        cn(
          formVariants.input.base({ ring }),
          withRenderProps(className)(values),
        )
      }
      {...props}
    />
  );
});
 
/* -------------------------------- Text Area ------------------------------- */
 
export type TextAreaProps = React.ComponentPropsWithRef<typeof RaTextArea> &
  Omit<VariantProps<typeof formVariants.input.base>, "multiline">;
 
export const TextArea = autoRef(
  ({ className, ring, ...props }: TextAreaProps) => {
    return (
      <RaTextArea
        className={(values) =>
          cn(
            formVariants.input.base({ ring, multiline: true }),
            withRenderProps(className)(values),
          )
        }
        {...props}
      />
    );
  },
);
 
/* ---------------------------------- Label --------------------------------- */
 
export type LabelProps = React.ComponentPropsWithRef<typeof RaLabel> & {
  as?: "aria" | "label" | "child";
};
 
export const Label = autoRef(
  ({ className, as = "aria", ...props }: LabelProps) => {
    const compoundClassName = cn(formVariants.label(), className);
 
    if (!props.children) return null;
 
    if (as === "aria") {
      return <RaLabel className={compoundClassName} {...props} />;
    } else {
      const Component = as === "child" ? Slot : "label";
      return <Component className={compoundClassName} {...props} />;
    }
  },
);
 
/* ------------------------------- Description ------------------------------ */
 
export type DescriptionProps = React.ComponentPropsWithRef<typeof Text>;
 
export const Description = autoRef(
  ({ className, ...props }: DescriptionProps) => {
    const compoundClassName = cn(formVariants.description(), className);
 
    if (!props.children) return null;
    return <Text slot="description" className={compoundClassName} {...props} />;
  },
);
 
/* ---------------------------------- Error --------------------------------- */
 
export type ErrorMessageProps = React.ComponentPropsWithRef<"div">;
 
export const ErrorMessage = autoRef(
  ({ className, ...props }: ErrorMessageProps) => {
    if (!props.children) return null;
 
    return <div className={cn(formVariants.error(), className)} {...props} />;
  },
);

Update the import paths to match your project setup.