Acronis UIKit
Legacy (shadcn-uikit)Components

Field

Layout primitives for composing accessible form fields — label, description, error, and grouping — without coupling to any form library.

Usage

import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldSet,
  FieldLegend,
  FieldContent,
  FieldTitle,
  FieldSeparator,
} from '@acronis-platform/shadcn-uikit';

Basic

A field with a label and description. Wire htmlFor on FieldLabel to the control's id.

This is your public display name.

View source
import * as React from 'react';
import {
  Field,
  FieldLabel,
  FieldDescription,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';

export function FieldBasic() {
  return (
    <div className="w-full max-w-sm">
      <Field>
        <FieldLabel htmlFor="username">Username</FieldLabel>
        <Input id="username" placeholder="johndoe" />
        <FieldDescription>This is your public display name.</FieldDescription>
      </Field>
    </div>
  );
}

With error

FieldError accepts an errors array of { message } objects. It renders nothing when the array is empty or undefined. Set data-invalid="true" on Field to apply the destructive colour to the label.

Must be strong and unique.

View source
import * as React from 'react';
import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';

export function FieldWithError() {
  return (
    <div className="w-full max-w-sm space-y-6">
      {/* Single error */}
      <Field data-invalid="true">
        <FieldLabel htmlFor="email-error">Email</FieldLabel>
        <Input
          id="email-error"
          type="email"
          defaultValue="notanemail"
          aria-invalid
        />
        <FieldError
          errors={[{ message: 'Please enter a valid email address.' }]}
        />
      </Field>

      {/* Multiple errors */}
      <Field data-invalid="true">
        <FieldLabel htmlFor="password-error">Password</FieldLabel>
        <Input
          id="password-error"
          type="password"
          defaultValue="abc"
          aria-invalid
        />
        <FieldDescription>Must be strong and unique.</FieldDescription>
        <FieldError
          errors={[
            { message: 'Must be at least 8 characters.' },
            { message: 'Must contain at least one number.' },
          ]}
        />
      </Field>
    </div>
  );
}

Horizontal orientation

The orientation prop controls how the label and control are arranged. "horizontal" places the label on the left and the control on the right. Use FieldContent inside FieldLabel to add a description block beside a toggle control.

View source
import * as React from 'react';
import {
  Field,
  FieldLabel,
  FieldContent,
  FieldDescription,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Switch } from '@acronis-platform/shadcn-uikit/react';
import { Checkbox } from '@acronis-platform/shadcn-uikit/react';

export function FieldHorizontal() {
  return (
    <div className="w-full max-w-lg space-y-4">
      {/* Label left, control right */}
      <Field orientation="horizontal">
        <FieldLabel htmlFor="h-name">Display name</FieldLabel>
        <Input id="h-name" placeholder="Your name" className="max-w-xs" />
      </Field>

      {/* With description block */}
      <Field orientation="horizontal">
        <FieldLabel htmlFor="h-switch">
          <FieldContent>
            <span>Email notifications</span>
            <FieldDescription>
              Get notified about account activity.
            </FieldDescription>
          </FieldContent>
        </FieldLabel>
        <Switch id="h-switch" defaultChecked />
      </Field>

      {/* Checkbox horizontal */}
      <Field orientation="horizontal">
        <FieldLabel htmlFor="h-checkbox">
          <FieldContent>
            <span>Marketing emails</span>
            <FieldDescription>
              Receive updates on new features.
            </FieldDescription>
          </FieldContent>
        </FieldLabel>
        <Checkbox id="h-checkbox" />
      </Field>
    </div>
  );
}

Available orientations: "vertical" (default) · "horizontal" · "responsive" (vertical on small screens, horizontal at @md).

FieldGroup

FieldGroup stacks multiple Field components with consistent spacing. Use it as the outer wrapper for any multi-field form section.

We will never share your email.

Up to 160 characters.

View source
import * as React from 'react';
import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldGroup,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';

export function FieldGroupDemo() {
  return (
    <div className="w-full max-w-md">
      <FieldGroup>
        <Field>
          <FieldLabel htmlFor="fg-name">Full name</FieldLabel>
          <Input id="fg-name" placeholder="Jane Doe" />
        </Field>

        <Field>
          <FieldLabel htmlFor="fg-email">Email</FieldLabel>
          <Input id="fg-email" type="email" placeholder="jane@example.com" />
          <FieldDescription>We will never share your email.</FieldDescription>
        </Field>

        <Field>
          <FieldLabel htmlFor="fg-role">Role</FieldLabel>
          <Select>
            <SelectTrigger id="fg-role">
              <SelectValue placeholder="Select a role" />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="admin">Admin</SelectItem>
              <SelectItem value="editor">Editor</SelectItem>
              <SelectItem value="viewer">Viewer</SelectItem>
            </SelectContent>
          </Select>
        </Field>

        <Field>
          <FieldLabel htmlFor="fg-bio">Bio</FieldLabel>
          <Textarea
            id="fg-bio"
            placeholder="A short bio…"
            className="resize-none"
          />
          <FieldDescription>Up to 160 characters.</FieldDescription>
        </Field>

        <Button>Save</Button>
      </FieldGroup>
    </div>
  );
}

FieldSet

FieldSet renders a native <fieldset> with a FieldLegend. Use it to semantically group related controls (radio buttons, checkbox lists, toggle switches) so screen readers announce the group name before each control.

Notifications
Theme
View source
import * as React from 'react';
import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldSet,
  FieldLegend,
  FieldGroup,
  FieldContent,
} from '@acronis-platform/shadcn-uikit/react';
import { Switch } from '@acronis-platform/shadcn-uikit/react';
import {
  RadioGroup,
  RadioGroupItem,
} from '@acronis-platform/shadcn-uikit/react';

export function FieldSetDemo() {
  return (
    <div className="w-full max-w-md space-y-8">
      {/* Switch group */}
      <FieldSet>
        <FieldLegend>Notifications</FieldLegend>
        <FieldGroup>
          <Field orientation="horizontal">
            <FieldLabel htmlFor="notif-email">
              <FieldContent>
                <span>Email</span>
                <FieldDescription>
                  Account activity and updates.
                </FieldDescription>
              </FieldContent>
            </FieldLabel>
            <Switch id="notif-email" defaultChecked />
          </Field>
          <Field orientation="horizontal">
            <FieldLabel htmlFor="notif-push">
              <FieldContent>
                <span>Push notifications</span>
                <FieldDescription>Critical alerts only.</FieldDescription>
              </FieldContent>
            </FieldLabel>
            <Switch id="notif-push" />
          </Field>
          <Field orientation="horizontal">
            <FieldLabel htmlFor="notif-marketing">
              <FieldContent>
                <span>Marketing</span>
                <FieldDescription>
                  New features and promotions.
                </FieldDescription>
              </FieldContent>
            </FieldLabel>
            <Switch id="notif-marketing" />
          </Field>
        </FieldGroup>
      </FieldSet>

      {/* Radio group */}
      <FieldSet>
        <FieldLegend>Theme</FieldLegend>
        <RadioGroup defaultValue="system" className="space-y-2">
          <Field orientation="horizontal">
            <FieldLabel htmlFor="theme-light">Light</FieldLabel>
            <RadioGroupItem id="theme-light" value="light" />
          </Field>
          <Field orientation="horizontal">
            <FieldLabel htmlFor="theme-dark">Dark</FieldLabel>
            <RadioGroupItem id="theme-dark" value="dark" />
          </Field>
          <Field orientation="horizontal">
            <FieldLabel htmlFor="theme-system">System</FieldLabel>
            <RadioGroupItem id="theme-system" value="system" />
          </Field>
        </RadioGroup>
      </FieldSet>
    </div>
  );
}

With TanStack Form

Because Field primitives have no dependency on React Hook Form, they compose naturally with TanStack Form. Pass field.state.meta.errors directly to FieldError.

Your public display name.

Up to 160 characters.

View source
import * as React from 'react';
import { useForm } from '@tanstack/react-form';
import * as z from 'zod';
import {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
} from '@acronis-platform/shadcn-uikit/react';
import { Input } from '@acronis-platform/shadcn-uikit/react';
import { Textarea } from '@acronis-platform/shadcn-uikit/react';
import { Button } from '@acronis-platform/shadcn-uikit/react';
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from '@acronis-platform/shadcn-uikit/react';

const schema = z.object({
  username: z
    .string()
    .min(2, 'At least 2 characters.')
    .max(30, 'Max 30 characters.'),
  email: z.string().email('Enter a valid email address.'),
  role: z.string().min(1, 'Please select a role.'),
  bio: z.string().max(160, 'Max 160 characters.').optional(),
});

type FormValues = z.infer<typeof schema>;

export function FieldTanstackForm() {
  const [submitted, setSubmitted] = React.useState<FormValues | null>(null);

  const form = useForm({
    defaultValues: { username: '', email: '', role: '', bio: '' },
    onSubmit: async ({ value }) => setSubmitted(value as FormValues),
  });

  if (submitted) {
    return (
      <div className="w-full max-w-md rounded-lg border p-6 space-y-3">
        <p className="text-sm font-medium text-green-600">Saved!</p>
        <pre className="text-xs bg-muted rounded p-3 overflow-auto">
          {JSON.stringify(submitted, null, 2)}
        </pre>
        <Button variant="outline" size="sm" onClick={() => setSubmitted(null)}>
          Edit again
        </Button>
      </div>
    );
  }

  return (
    <div className="w-full max-w-md">
      <form
        onSubmit={(e) => {
          e.preventDefault();
          e.stopPropagation();
          form.handleSubmit();
        }}
      >
        <FieldGroup>
          <form.Field
            name="username"
            validators={{
              onChange: ({ value }) => {
                const result = schema.shape.username.safeParse(value);
                return result.success
                  ? undefined
                  : result.error.issues[0]?.message;
              },
            }}
          >
            {(field) => (
              <Field
                data-invalid={
                  field.state.meta.isTouched &&
                  field.state.meta.errors.length > 0
                    ? 'true'
                    : undefined
                }
              >
                <FieldLabel htmlFor={field.name}>Username</FieldLabel>
                <Input
                  id={field.name}
                  placeholder="johndoe"
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                  aria-invalid={
                    field.state.meta.isTouched &&
                    field.state.meta.errors.length > 0
                  }
                />
                <FieldDescription>Your public display name.</FieldDescription>
                {field.state.meta.isTouched && (
                  <FieldError
                    errors={field.state.meta.errors.map((e) => ({
                      message: e?.toString(),
                    }))}
                  />
                )}
              </Field>
            )}
          </form.Field>

          <form.Field
            name="email"
            validators={{
              onChange: ({ value }) => {
                const result = schema.shape.email.safeParse(value);
                return result.success
                  ? undefined
                  : result.error.issues[0]?.message;
              },
            }}
          >
            {(field) => (
              <Field
                data-invalid={
                  field.state.meta.isTouched &&
                  field.state.meta.errors.length > 0
                    ? 'true'
                    : undefined
                }
              >
                <FieldLabel htmlFor={field.name}>Email</FieldLabel>
                <Input
                  id={field.name}
                  type="email"
                  placeholder="jane@example.com"
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                  aria-invalid={
                    field.state.meta.isTouched &&
                    field.state.meta.errors.length > 0
                  }
                />
                {field.state.meta.isTouched && (
                  <FieldError
                    errors={field.state.meta.errors.map((e) => ({
                      message: e?.toString(),
                    }))}
                  />
                )}
              </Field>
            )}
          </form.Field>

          <form.Field
            name="role"
            validators={{
              onChange: ({ value }) => {
                const result = schema.shape.role.safeParse(value);
                return result.success
                  ? undefined
                  : result.error.issues[0]?.message;
              },
            }}
          >
            {(field) => (
              <Field
                data-invalid={
                  field.state.meta.isTouched &&
                  field.state.meta.errors.length > 0
                    ? 'true'
                    : undefined
                }
              >
                <FieldLabel htmlFor={field.name}>Role</FieldLabel>
                <Select
                  value={field.state.value}
                  onValueChange={(val) => field.handleChange(val ?? '')}
                >
                  <SelectTrigger id={field.name} onBlur={field.handleBlur}>
                    <SelectValue placeholder="Select a role" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="admin">Admin</SelectItem>
                    <SelectItem value="editor">Editor</SelectItem>
                    <SelectItem value="viewer">Viewer</SelectItem>
                  </SelectContent>
                </Select>
                {field.state.meta.isTouched && (
                  <FieldError
                    errors={field.state.meta.errors.map((e) => ({
                      message: e?.toString(),
                    }))}
                  />
                )}
              </Field>
            )}
          </form.Field>

          <form.Field
            name="bio"
            validators={{
              onChange: ({ value }) => {
                const result = schema.shape.bio.safeParse(value);
                return result.success
                  ? undefined
                  : result.error.issues[0]?.message;
              },
            }}
          >
            {(field) => (
              <Field>
                <FieldLabel htmlFor={field.name}>Bio</FieldLabel>
                <Textarea
                  id={field.name}
                  placeholder="Tell us a little about yourself"
                  className="resize-none"
                  value={field.state.value}
                  onChange={(e) => field.handleChange(e.target.value)}
                  onBlur={field.handleBlur}
                />
                <FieldDescription>Up to 160 characters.</FieldDescription>
                {field.state.meta.isTouched && (
                  <FieldError
                    errors={field.state.meta.errors.map((e) => ({
                      message: e?.toString(),
                    }))}
                  />
                )}
              </Field>
            )}
          </form.Field>

          <form.Subscribe selector={(s) => [s.canSubmit, s.isSubmitting]}>
            {([canSubmit, isSubmitting]) => (
              <Button type="submit" disabled={!canSubmit || isSubmitting}>
                {isSubmitting ? 'Saving…' : 'Save profile'}
              </Button>
            )}
          </form.Subscribe>
        </FieldGroup>
      </form>
    </div>
  );
}

Field vs Form

The library ships two parallel sets of form-field primitives:

Field / FieldLabel / FieldErrorFormItem / FormLabel / FormMessage
CouplingNone — pure layoutReact Hook Form (useFormContext)
Use withTanStack Form, native HTML, any libReact Hook Form only
Error sourceerrors prop (array)Reads from RHF useFormContext automatically
Label ↔ controlManual htmlFor / idAuto-wired via FormItem context
Orientation variantsvertical · horizontal · responsiveVertical only
GroupingFieldGroup, FieldSet, FieldLegend<div className="space-y-4">

Rule of thumb: use Field when you are not using React Hook Form. Use FormItem and friends when you are.

API Reference

Field

Prop

Type

FieldError

Accepts the standard div props plus:

PropTypeDescription
errorsArray<{ message?: string } | undefined>Error objects to display. Renders nothing when empty or undefined.
childrenReactNodeCustom error content. Takes precedence over errors.

FieldLegend

PropTypeDefaultDescription
variant"legend" | "label""legend""legend" uses text-base; "label" uses text-sm for tighter sections.
Edit on GitHub

On this page