# Field

> Label, description and error wiring for any control, plus a standalone Label.

Category: Forms. Docs: https://neonkit.heyaadi.com/components/field

## Installation

```bash
npx shadcn@latest add https://neonkit.heyaadi.com/r/field.json
```

With the `@neonkit` namespace configured: `npx shadcn@latest add @neonkit/field`.

npm dependencies: none. Also needs `lib/utils.ts` (`cn`) and `lib/neon.ts` (`neonVars`), which the CLI adds.

## Usage

```tsx
import { Field } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

<Field orientation="horizontal" label="Preview deployments" description="Build every pull request on its own URL.">
  <Switch defaultChecked />
</Field>
```

## Examples

### Settings

One Field wraps a Switch, a native select and a Slider. Each control gets its id, label and description wired without extra props.

```tsx
"use client";

import * as React from "react";
import { Field } from "@/components/ui/field";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";

const chevron = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-opacity='0.4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")`;

export default function FieldDemo() {
  const [concurrency, setConcurrency] = React.useState([4]);

  return (
    <div className="grid w-full max-w-sm gap-5 rounded-xl border border-white/10 bg-white/[0.03] p-5">
      <Field
        orientation="horizontal"
        label="Preview deployments"
        description="Build every pull request on its own URL."
      >
        <Switch defaultChecked />
      </Field>

      <div className="h-px bg-white/[0.06]" />

      <Field label="Default region" description="New projects deploy here unless they say otherwise.">
        <select
          defaultValue="fra1"
          style={{ backgroundImage: chevron }}
          className="h-10 w-full cursor-pointer appearance-none rounded-lg border border-white/10 bg-white/[0.03] bg-[length:16px] bg-[right_12px_center] bg-no-repeat pl-3 pr-9 text-sm text-white/90 outline-none [color-scheme:dark] transition-[border-color,box-shadow] duration-150 hover:border-white/20 focus-visible:border-[rgb(var(--neon)/0.8)] focus-visible:shadow-[0_0_0_1px_rgb(var(--neon)/0.2),0_0_20px_-4px_rgb(var(--neon)/0.5)]"
        >
          <option value="fra1">Frankfurt · eu-central-1</option>
          <option value="lhr1">London · eu-west-2</option>
          <option value="iad1">Washington, D.C. · us-east-1</option>
          <option value="sfo1">San Francisco · us-west-1</option>
          <option value="hnd1">Tokyo · ap-northeast-1</option>
        </select>
      </Field>

      <Field
        label={
          <>
            Build concurrency
            <span aria-hidden className="ml-2 font-mono text-xs tabular-nums text-white/50">
              {concurrency[0]} of 8
            </span>
          </>
        }
        description="Builds that can run at once across the team."
      >
        <Slider value={concurrency} onValueChange={setConcurrency} min={1} max={8} />
      </Field>
    </div>
  );
}
```

### In a form

Every NeonKit control submits under its name and goes back to its starting value on reset. Required ones stop an empty submit with the browser's own message. Send it empty, then fill it in.

```tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { DatePicker } from "@/components/ui/date-picker";
import { Field } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { Select } from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";

const roles = [
  { value: "viewer", label: "Viewer", description: "Sees deploys and logs" },
  { value: "developer", label: "Developer", description: "Deploys to preview" },
  { value: "admin", label: "Admin", description: "Manages billing and members" },
];

export default function FieldForm() {
  const [sent, setSent] = React.useState<[string, string][] | null>(null);

  function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    setSent(Array.from(data.entries(), ([key, value]) => [key, String(value)]));
  }

  return (
    <form
      onSubmit={submit}
      onReset={() => setSent(null)}
      className="grid w-full max-w-sm gap-5"
    >
      <Input name="email" type="email" label="Email" placeholder="maya@northwind.io" required autoComplete="off" />
      <Select name="role" label="Role" placeholder="Choose a role" options={roles} required />
      <Field label="Access" description="Production access needs an admin to approve it.">
        <ToggleGroup name="access" defaultValue="preview">
          <ToggleGroupItem value="preview">Preview</ToggleGroupItem>
          <ToggleGroupItem value="production">Production</ToggleGroupItem>
        </ToggleGroup>
      </Field>
      <DatePicker name="expires" label="Access expires" placeholder="Never" />
      <Field orientation="horizontal" label="Email a welcome guide">
        <Switch name="welcome" defaultChecked />
      </Field>
      <Checkbox name="sso" label="Require single sign-on" description="They can't sign in with a password." />

      <div className="flex items-center justify-end gap-2 pt-1">
        <Button type="reset" variant="ghost" size="sm">
          Reset
        </Button>
        <Button type="submit" variant="solid" size="sm">
          Send invite
        </Button>
      </div>

      <dl aria-live="polite" className="grid min-h-5 gap-1 text-xs">
        {sent?.map(([key, value]) => (
          <div key={key} className="flex justify-between gap-4">
            <dt className="text-white/55">{key}</dt>
            <dd className="truncate font-mono text-white/90">{value || "empty"}</dd>
          </div>
        ))}
      </dl>
    </form>
  );
}
```

### Custom control

A control whose focusable parts sit inside a wrapper reads its wiring from useFieldControl() and spreads it onto the group.

```tsx
"use client";

import * as React from "react";
import { Field, useFieldControl } from "@/components/ui/field";
import { cn } from "@/lib/utils";

const days = [
  { value: "mon", short: "Mo", name: "Monday" },
  { value: "tue", short: "Tu", name: "Tuesday" },
  { value: "wed", short: "We", name: "Wednesday" },
  { value: "thu", short: "Th", name: "Thursday" },
  { value: "fri", short: "Fr", name: "Friday" },
  { value: "sat", short: "Sa", name: "Saturday" },
  { value: "sun", short: "Su", name: "Sunday" },
];

// A custom control: the wrapper is a group, so it reads the field's id and
// aria wiring from the hook instead of from props.
function DayPicker({ value, onValueChange }: { value: string[]; onValueChange: (value: string[]) => void }) {
  const field = useFieldControl();

  return (
    <div role="group" {...field} className="flex flex-wrap gap-1.5">
      {days.map((day) => {
        const pressed = value.includes(day.value);
        return (
          <button
            key={day.value}
            type="button"
            aria-pressed={pressed}
            onClick={() =>
              onValueChange(pressed ? value.filter((v) => v !== day.value) : [...value, day.value])
            }
            className={cn(
              "h-9 w-10 cursor-pointer rounded-lg border text-xs font-medium outline-none",
              "transition-[border-color,background-color,color,box-shadow,transform,scale] duration-150 ease-out active:scale-[0.97]",
              "focus-visible:ring-2 focus-visible:ring-[rgb(var(--neon)/0.6)] focus-visible:ring-offset-2 focus-visible:ring-offset-[#05060a]",
              pressed
                ? "border-[rgb(var(--neon)/0.55)] bg-[rgb(var(--neon)/0.12)] text-[rgb(var(--neon))] shadow-[0_0_14px_-2px_rgb(var(--neon)/0.5)]"
                : "border-white/10 bg-white/[0.03] text-white/55 hover:border-white/20 hover:text-white/85",
            )}
          >
            {day.short}
            {/* Spoken as "Mo, Monday": the name starts with what's on the button, so voice control finds it. */}
            <span className="sr-only">, {day.name}</span>
          </button>
        );
      })}
    </div>
  );
}

export default function FieldCustomControl() {
  const [quiet, setQuiet] = React.useState(["sat", "sun"]);

  return (
    <Field
      label="Quiet days"
      description="No alerts on these days, except incidents marked critical."
      error={quiet.length === days.length ? "Leave at least one day for alerts to reach you." : undefined}
      className="max-w-sm"
    >
      <DayPicker value={quiet} onValueChange={setQuiet} />
    </Field>
  );
}
```

### Validation

Pass error to swap the description for a message. The field turns red, the control gets aria-invalid, and required shows a neon dot.

```tsx
"use client";

import * as React from "react";
import { Button } from "@/components/ui/button";
import { Field } from "@/components/ui/field";
import { Switch } from "@/components/ui/switch";

const control =
  "h-10 w-full rounded-lg border border-white/10 bg-white/[0.03] text-sm text-white/90 outline-none [color-scheme:dark] transition-[border-color,background-color,box-shadow] duration-150 placeholder:text-white/30 hover:border-white/20 focus-visible:border-[rgb(var(--neon)/0.8)] focus-visible:bg-[rgb(var(--neon)/0.04)] focus-visible:shadow-[0_0_0_1px_rgb(var(--neon)/0.2),0_0_20px_-4px_rgb(var(--neon)/0.5)] aria-[invalid=true]:border-[rgb(var(--neon)/0.55)]";

const chevron = `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-opacity='0.4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E")`;

type Errors = { code?: string; term?: string };

export default function FieldValidation() {
  const formRef = React.useRef<HTMLFormElement>(null);
  const [errors, setErrors] = React.useState<Errors>({});
  const [attempt, setAttempt] = React.useState(0);
  const [status, setStatus] = React.useState("");

  // After a failed submit, move focus to the first field that needs attention.
  React.useEffect(() => {
    if (attempt) formRef.current?.querySelector<HTMLElement>('[aria-invalid="true"]')?.focus();
  }, [attempt]);

  function submit(event: React.FormEvent<HTMLFormElement>) {
    event.preventDefault();
    const data = new FormData(event.currentTarget);
    const code = String(data.get("auth-code") ?? "").trim();
    const term = String(data.get("term") ?? "");
    const next: Errors = {
      code: !code
        ? "Enter the auth code from your current registrar."
        : code.length < 8
          ? `Auth codes are at least 8 characters. This one has ${code.length}.`
          : undefined,
      term: term ? undefined : "Choose how long to renew for after the transfer.",
    };
    setErrors(next);
    if (next.code || next.term) {
      setStatus("");
      setAttempt((count) => count + 1);
    } else {
      setStatus("Transfer requested for nightjar.studio. We'll email you when it lands.");
    }
  }

  return (
    <form ref={formRef} noValidate onSubmit={submit} className="grid w-full max-w-sm gap-5">
      <Field
        label="Auth code"
        required
        description="Also called an EPP code. Your current registrar sends it on request."
        error={errors.code}
      >
        <input
          name="auth-code"
          autoComplete="off"
          spellCheck={false}
          placeholder="e.g. 7Xq#vR2m!pL9"
          onChange={() => errors.code && setErrors((e) => ({ ...e, code: undefined }))}
          className={`${control} px-3 font-mono`}
        />
      </Field>

      <Field label="Renew for" required error={errors.term}>
        <select
          name="term"
          defaultValue=""
          style={{ backgroundImage: chevron }}
          onChange={() => errors.term && setErrors((e) => ({ ...e, term: undefined }))}
          className={`${control} cursor-pointer appearance-none bg-[length:16px] bg-[right_12px_center] bg-no-repeat pl-3 pr-9`}
        >
          <option value="" disabled>
            Choose a term
          </option>
          <option value="1">1 year · $14</option>
          <option value="2">2 years · $27</option>
          <option value="5">5 years · $64</option>
        </select>
      </Field>

      <Field
        orientation="horizontal"
        label="WHOIS privacy"
        description="Hide your contact details from public lookups."
      >
        <Switch name="privacy" defaultChecked />
      </Field>

      <div className="grid gap-2">
        <Button type="submit" variant="solid" className="w-full">
          Request transfer
        </Button>
        <p aria-live="polite" className="min-h-5 text-xs leading-5 text-white/55">
          {status}
        </p>
      </div>
    </form>
  );
}
```

### Building blocks

Label, FieldDescription and FieldError on their own, for layouts Field doesn't cover, like an action beside the label.

```tsx
"use client";

import * as React from "react";
import { FieldDescription, FieldError, Label } from "@/components/ui/field";
import { Input } from "@/components/ui/input";

export default function FieldBuildingBlocks() {
  const [password, setPassword] = React.useState("");
  const [touched, setTouched] = React.useState(false);
  const [visible, setVisible] = React.useState(false);
  const tooShort = touched && password.length > 0 && password.length < 12;

  return (
    <div className="grid w-full max-w-sm gap-1.5">
      <div className="flex items-baseline justify-between gap-3">
        <Label htmlFor="new-password" required>
          New password
        </Label>
        <button
          type="button"
          aria-pressed={visible}
          aria-controls="new-password"
          onClick={() => setVisible((v) => !v)}
          className="cursor-pointer rounded-md text-xs text-white/55 outline-none transition-colors hover:text-white/90 focus-visible:text-[rgb(var(--neon))]"
        >
          {visible ? "Hide" : "Show"}
        </button>
      </div>
      <Input
        id="new-password"
        type={visible ? "text" : "password"}
        autoComplete="new-password"
        required
        value={password}
        onChange={(event) => setPassword(event.target.value)}
        onBlur={() => setTouched(true)}
        error={tooShort}
        aria-describedby="new-password-message"
      />
      {tooShort ? (
        <FieldError id="new-password-message">
          {12 - password.length} more {12 - password.length === 1 ? "character" : "characters"} to go.
        </FieldError>
      ) : (
        <FieldDescription id="new-password-message">
          At least 12 characters. A short sentence you&apos;ll remember works well.
        </FieldDescription>
      )}
    </div>
  );
}
```

## API reference

### Field

Wraps a single control element and clones it with id, aria-describedby, aria-invalid and required. Also accepts native div attributes, which go to the outer block.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` | `ReactNode` | — | Visible label. Points at the control with htmlFor, or aria-labelledby for controls a label can't target. |
| `description` | `ReactNode` | — | Helper text under the control. |
| `error` | `ReactNode` | — | Red invalid state and message. Replaces description. Pass true for the state without a message. |
| `required` | `boolean` | `false` | Shows a neon dot after the label and sets required on the control. |
| `id` | `string` | — | The control's id. Defaults to the child's own id, then a generated one. |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | Horizontal puts the label and message on the left and the control on the right. Suits switches and other compact controls. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Label size. Match it to the control's size. |
| `color` | `NeonColor` | `inherited` | Sets --neon-accent for the control inside. Red while error is set. |
| `children` | `ReactElement` | — | The control. One element; it receives the wiring as props. |

### useFieldControl

Returns the props a custom control inside a Field should spread onto its focusable element or group. Returns an empty object outside a Field.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Matches the label's htmlFor. |
| `aria-labelledby` | `string` | — | The label's id, for elements a label can't target. |
| `aria-describedby` | `string` | — | The description or error message id. |
| `aria-invalid` | `true \| undefined` | — | Set while the field has an error. |
| `required` | `true \| undefined` | — | Set when the field is required. Map it to aria-required if your role supports it. |

### Label

Native label with the same type scale as Input, Select and Textarea.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `htmlFor` | `string` | — | The control's id. Inside a Field it defaults to the field's control. |
| `required` | `boolean` | `false` | Shows the neon dot. Set required on the control as well so it is announced. |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | Text size. |

### FieldDescription

Helper text paragraph. Give it an id and point the control's aria-describedby at it.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Referenced by the control's aria-describedby. |

### FieldError

Red message paragraph. Renders nothing when it has no children.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `id` | `string` | — | Referenced by the control's aria-describedby. |

## Accessibility

- Native inputs, selects, textareas and buttons are labelled with htmlFor; other controls get aria-labelledby, so a slider thumb or a custom group is still named.
- Clicking the label of a control that a label can't focus moves focus to its first focusable part.
- The description or error is linked with aria-describedby, merged with any aria-describedby the control already has.
- The required dot is decorative. Field puts required on the control so assistive tech announces it.
- NeonKit inputs that already take label, description and error props don't need a Field; use it for everything else.

## Source

`components/ui/field.tsx`

```tsx
"use client";

import * as React from "react";

import { cn } from "@/lib/utils";
import { neonVars, type NeonColor } from "@/lib/neon";

const INVALID = "#ff4d5e";

// Same label scale as Input, Select and Textarea, so mixed forms line up.
const labelSizes = {
  sm: "text-xs",
  md: "text-sm",
  lg: "text-sm",
} as const;

// Elements a <label for> can point at. Anything else gets aria-labelledby instead.
const LABELABLE = new Set(["input", "select", "textarea", "button", "meter", "output", "progress"]);
const FOCUSABLE =
  'input:not([type="hidden"]):not(:disabled), select:not(:disabled), textarea:not(:disabled), button:not(:disabled), [tabindex]:not([tabindex="-1"])';

type FieldContextValue = {
  id: string;
  labelId?: string;
  describedBy?: string;
  invalid: boolean;
  required: boolean;
};

const FieldContext = React.createContext<FieldContextValue | null>(null);

export interface FieldControlProps {
  id?: string;
  "aria-labelledby"?: string;
  "aria-describedby"?: string;
  "aria-invalid"?: true;
  required?: true;
}

/**
 * Props for a custom control inside a <Field>. Spread them onto the focusable
 * element. Outside a Field it returns an empty object.
 */
function useFieldControl(): FieldControlProps {
  const field = React.useContext(FieldContext);
  if (!field) return {};
  return {
    id: field.id,
    "aria-labelledby": field.labelId,
    "aria-describedby": field.describedBy,
    "aria-invalid": field.invalid || undefined,
    required: field.required || undefined,
  };
}

export interface LabelProps extends React.LabelHTMLAttributes<HTMLLabelElement> {
  /** Shows a small neon dot after the text. Set `required` on the control too. */
  required?: boolean;
  size?: keyof typeof labelSizes;
}

const Label = React.forwardRef<HTMLLabelElement, LabelProps>(
  ({ className, required, size = "md", htmlFor, children, ...props }, ref) => {
    const field = React.useContext(FieldContext);
    const isRequired = required ?? field?.required ?? false;

    return (
      <label
        ref={ref}
        htmlFor={htmlFor ?? field?.id}
        className={cn("w-fit font-medium text-white/80", labelSizes[size], className)}
        {...props}
      >
        {children}
        {isRequired && (
          <span
            aria-hidden
            title="Required"
            className={cn(
              "[--neon:var(--neon-accent,34_211_238)]",
              "relative -top-px ml-1.5 inline-block size-[5px] rounded-full align-middle",
              "bg-[rgb(var(--neon))] shadow-[0_0_4px_rgb(var(--neon)/CALC(0.9*var(--neon-glow,1))),0_0_10px_rgb(var(--neon)/CALC(0.5*var(--neon-glow,1)))]",
            )}
          />
        )}
      </label>
    );
  },
);
Label.displayName = "Label";

const FieldDescription = React.forwardRef<
  HTMLParagraphElement,
  React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
  <p ref={ref} className={cn("text-xs leading-5 text-white/55", className)} {...props} />
));
FieldDescription.displayName = "FieldDescription";

const FieldError = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HTMLParagraphElement>>(
  ({ className, children, ...props }, ref) => {
    if (children == null || children === false || children === "") return null;
    return (
      <p ref={ref} className={cn("text-xs leading-5 text-[#ff4d5e]", className)} {...props}>
        {children}
      </p>
    );
  },
);
FieldError.displayName = "FieldError";

type ControlElementProps = {
  id?: string;
  "aria-label"?: string;
  "aria-labelledby"?: string;
  "aria-describedby"?: string;
  "aria-invalid"?: React.AriaAttributes["aria-invalid"];
};

export interface FieldProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "color"> {
  label?: React.ReactNode;
  /** Helper text under the control. Hidden while `error` is set. */
  description?: React.ReactNode;
  /** Marks the field invalid and shows this message. Pass `true` for the red state alone. */
  error?: React.ReactNode;
  /** Shows the neon dot on the label and sets `required` on the control. */
  required?: boolean;
  /** The control's id. Defaults to the child's own id, then a generated one. */
  id?: string;
  /** Palette name or any hex color, passed down to the control. Red while `error` is set. */
  color?: NeonColor;
  /** Label text size, matching the control's size. */
  size?: keyof typeof labelSizes;
  /** "horizontal" puts the label and message on the left and the control on the right. */
  orientation?: "vertical" | "horizontal";
}

const Field = React.forwardRef<HTMLDivElement, FieldProps>(
  (
    {
      className,
      style,
      label,
      description,
      error,
      required = false,
      id: idProp,
      color,
      size = "md",
      orientation = "vertical",
      children,
      ...props
    },
    ref,
  ) => {
    const autoId = React.useId();
    const child =
      React.isValidElement<ControlElementProps>(children) && children.type !== React.Fragment
        ? children
        : null;
    const id = idProp ?? child?.props.id ?? autoId;
    const labelId = label ? `${id}-label` : undefined;

    const invalid = Boolean(error);
    const message = invalid ? (error === true ? null : error) : description;
    const messageId = message ? `${id}-${invalid ? "error" : "description"}` : undefined;

    const context = React.useMemo<FieldContextValue>(
      () => ({ id, labelId, describedBy: messageId, invalid, required }),
      [id, labelId, messageId, invalid, required],
    );

    let control: React.ReactNode = children;
    if (child) {
      const own = child.props;
      const labelable = typeof child.type === "string" && LABELABLE.has(child.type);
      const named = own["aria-label"] != null || own["aria-labelledby"] != null;
      control = React.cloneElement(child, {
        id,
        "aria-describedby": [own["aria-describedby"], messageId].filter(Boolean).join(" ") || undefined,
        "aria-invalid": invalid || own["aria-invalid"],
        ...(required ? { required: true } : null),
        ...(labelId && !labelable && !named ? { "aria-labelledby": labelId } : null),
      } as ControlElementProps);
    }

    // A label only focuses labelable elements. For the rest (a slider thumb,
    // a custom group), move focus to the first focusable part ourselves.
    function handleLabelClick(event: React.MouseEvent<HTMLLabelElement>) {
      const target = document.getElementById(id);
      if (!target || LABELABLE.has(target.tagName.toLowerCase())) return;
      const focusable = target.matches(FOCUSABLE) ? target : target.querySelector<HTMLElement>(FOCUSABLE);
      if (!focusable) return;
      event.preventDefault();
      focusable.focus();
    }

    const labelEl = label && (
      <Label id={labelId} htmlFor={id} required={required} size={size} onClick={handleLabelClick}>
        {label}
      </Label>
    );

    const messageEl =
      message &&
      (invalid ? (
        <FieldError id={messageId}>{message}</FieldError>
      ) : (
        <FieldDescription id={messageId}>{message}</FieldDescription>
      ));

    return (
      <FieldContext.Provider value={context}>
        <div
          ref={ref}
          data-invalid={invalid ? "" : undefined}
          data-orientation={orientation}
          style={neonVars(invalid ? INVALID : color, style)}
          className={cn(
            "[--neon:var(--neon-accent,34_211_238)]",
            orientation === "horizontal"
              ? "flex w-full items-center justify-between gap-x-6"
              : "grid w-full gap-1.5",
            className,
          )}
          {...props}
        >
          {orientation === "horizontal" ? (
            <>
              {(labelEl || messageEl) && (
                <div className="grid min-w-0 gap-0.5">
                  {labelEl}
                  {messageEl}
                </div>
              )}
              <div className="flex shrink-0 items-center">{control}</div>
            </>
          ) : (
            <>
              {labelEl}
              {control}
              {messageEl}
            </>
          )}
        </div>
      </FieldContext.Provider>
    );
  },
);
Field.displayName = "Field";

export { Field, Label, FieldDescription, FieldError, useFieldControl };
```
