Vela UI Docsvenizia.ai

Field

Composes a label, a control, a description and an error message into one form field.

npx shadcn@latest add @velaui/field
npx shadcn@latest add @velaui/field

Examples

  • Default
  • Invalid
  • Horizontal

View code

Field · Default

Installation

pnpm dlx velaui add @velaui/field
pnpm dlx velaui add @velaui/field

Code

field-default.tsx
import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

export default function Example() {
  return (
    <FieldGroup className="w-full max-w-sm">
      <Field>
        <FieldLabel htmlFor="project-name">Project name</FieldLabel>
        <Input id="project-name" placeholder="Quarterly report" />
        <FieldDescription>Shown to everyone in the workspace.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel htmlFor="project-summary">Summary</FieldLabel>
        <Textarea id="project-summary" placeholder="What is this project about?" />
      </Field>
    </FieldGroup>
  );
}

View code

Field · Invalid

Installation

pnpm dlx velaui add @velaui/field
pnpm dlx velaui add @velaui/field

Code

field-invalid.tsx
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";

export default function Example() {
  return (
    <Field data-invalid="true" className="w-full max-w-sm">
      <FieldLabel htmlFor="email">Email</FieldLabel>
      <Input id="email" type="email" defaultValue="ava@lindensupply" aria-invalid="true" />
      <FieldError errors={[{ message: "Enter a complete email address." }]} />
    </Field>
  );
}

View code

Field · Horizontal

Installation

pnpm dlx velaui add @velaui/field
pnpm dlx velaui add @velaui/field

Code

field-horizontal.tsx
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldLabel,
} from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";

export default function Example() {
  return (
    <Field orientation="horizontal" className="w-full max-w-sm">
      <FieldContent>
        <FieldLabel htmlFor="digest">Weekly digest</FieldLabel>
        <FieldDescription>A summary of activity every Monday.</FieldDescription>
      </FieldContent>
      <Switch id="digest" defaultChecked />
    </Field>
  );
}

Usage

import {
  Field,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";

export default function Example() {
  return (
    <FieldGroup className="w-full max-w-sm">
      <Field>
        <FieldLabel htmlFor="project-name">Project name</FieldLabel>
        <Input id="project-name" placeholder="Quarterly report" />
        <FieldDescription>Shown to everyone in the workspace.</FieldDescription>
      </Field>
      <Field>
        <FieldLabel htmlFor="project-summary">Summary</FieldLabel>
        <Textarea id="project-summary" placeholder="What is this project about?" />
      </Field>
    </FieldGroup>
  );
}

Props

Field

PropTypeDefault
orientation"horizontal" | "vertical" | "responsive""vertical"

Takes these and every prop of FieldProps.

FieldLabel

Takes every prop of FieldLabelProps.

FieldDescription

Takes every prop of FieldDescriptionProps.

FieldError

PropTypeDefault
errors({ message?: string; } | undefined)[]—

Takes these and every prop of FieldErrorProps.

FieldGroup

Takes every prop of FieldGroupProps.

FieldLegend

PropTypeDefault
variant"label" | "legend""legend"

Takes these and every prop of FieldLegendProps.

FieldSeparator

PropTypeDefault
childrenstring | number | bigint | boolean | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactElement<unknown, string | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").JSXElementConstructor<any>> | Iterable<import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactNode> | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactPortal | Promise<string | number | bigint | boolean | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactPortal | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactElement<unknown, string | import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").JSXElementConstructor<any>> | Iterable<import("/app/node_modules/.bun/@types+react@19.2.17/node_modules/@types/react/index").ReactNode> | null | undefined>—

Takes these and every prop of FieldSeparatorProps.

FieldSet

Takes every prop of FieldSetProps.

FieldContent

Takes every prop of FieldContentProps.

FieldTitle

Takes every prop of FieldTitleProps.

Dependencies

npm
class-variance-authority@0.7.1
Installs with it
LabelSeparator

Source

components/ui/field.tsx
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";

export type FieldSetProps = React.ComponentProps<"fieldset">;

function FieldSet({ className, ...props }: FieldSetProps) {
  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}
    />
  );
}

export type FieldLegendProps = React.ComponentProps<"legend"> & { variant?: "legend" | "label" };

function FieldLegend({
  className,
  variant = "legend",
  ...props
}: FieldLegendProps) {
  return (
    <legend
      data-slot="field-legend"
      data-variant={variant}
      className={cn(
        "mb-1.5 font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
        className,
      )}
      {...props}
    />
  );
}

export type FieldGroupProps = React.ComponentProps<"div">;

function FieldGroup({ className, ...props }: FieldGroupProps) {
  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-negative-middle",
  {
    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",
    },
  },
);

export type FieldProps = React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>;

function Field({
  className,
  orientation = "vertical",
  ...props
}: FieldProps) {
  return (
    <div
      role="group"
      data-slot="field"
      data-orientation={orientation}
      className={cn(fieldVariants({ orientation }), className)}
      {...props}
    />
  );
}

export type FieldContentProps = React.ComponentProps<"div">;

function FieldContent({ className, ...props }: FieldContentProps) {
  return (
    <div
      data-slot="field-content"
      className={cn(
        "group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
        className,
      )}
      {...props}
    />
  );
}

export type FieldLabelProps = React.ComponentProps<typeof Label>;

function FieldLabel({
  className,
  ...props
}: FieldLabelProps) {
  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-lg has-[>[data-slot=field]]:border *: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}
    />
  );
}

export type FieldTitleProps = React.ComponentProps<"div">;

function FieldTitle({ className, ...props }: FieldTitleProps) {
  return (
    <div
      data-slot="field-label"
      className={cn(
        "flex w-fit items-center gap-2 text-sm font-medium group-data-[disabled=true]/field:opacity-50",
        className,
      )}
      {...props}
    />
  );
}

export type FieldDescriptionProps = React.ComponentProps<"p">;

function FieldDescription({ className, ...props }: FieldDescriptionProps) {
  return (
    <p
      data-slot="field-description"
      className={cn(
        "text-left text-sm leading-normal font-normal text-text-muted 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-text-strong",
        className,
      )}
      {...props}
    />
  );
}

export type FieldSeparatorProps = React.ComponentProps<"div"> & { children?: React.ReactNode };

function FieldSeparator({
  children,
  className,
  ...props
}: FieldSeparatorProps) {
  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-bg-main px-2 text-text-muted"
          data-slot="field-separator-content"
        >
          {children}
        </span>
      )}
    </div>
  );
}

export type FieldErrorProps = React.ComponentProps<"div"> & { errors?: Array<{ message?: string } | undefined> };

function FieldError({
  className,
  children,
  errors,
  ...props
}: FieldErrorProps) {
  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-negative-middle", className)}
      {...props}
    >
      {content}
    </div>
  );
}

export {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldContent,
  FieldTitle,
};