Skip to main content
Nexus Design System
GitHub

Field

Field lays out a label, control, description and error as one form field. Use it for any labeled form control. For a group of related fields, wrap them in FieldSet or FieldGroup.

We never share it.

'use client';

import { Field, FieldDescription, FieldLabel } from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldDefault() {
  return (
    <div className="nx:w-80">
      <Field>
        <FieldLabel htmlFor="field-email">Email</FieldLabel>
        <Input id="field-email" type="email" placeholder="you@example.com" />
        <FieldDescription>We never share it.</FieldDescription>
      </Field>
    </div>
  );
}

This example also needs: Input.

Installation

npx nxds-cli@latest add field

Run npx nxds-cli@latest init first if nxds isn’t set up in your app yet. See Install.

Usage

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldRequiredIndicator,
  FieldSeparator,
  FieldSet,
} from '@/components/nxds/components/field/field';

Composition

Field
├── FieldLabel
├── FieldDescription
└── FieldError
FieldGroup
├── Field
│   ├── FieldLabel
│   │   └── FieldRequiredIndicator
│   └── FieldContent
│       ├── FieldLabel
│       └── FieldDescription
└── FieldSeparator
FieldSet
├── FieldLegend
├── FieldDescription
└── Field
    └── FieldLabel

Examples

Orientations

Set orientation to vertical, horizontal or responsive, which follows the FieldGroup's width.

Shown on your public profile.

'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldOrientations() {
  return (
    <FieldGroup className="nx:w-80">
      <Field orientation="vertical">
        <FieldLabel htmlFor="field-o-name">Name</FieldLabel>
        <Input id="field-o-name" placeholder="Ada Lovelace" />
      </Field>
      <Field orientation="horizontal">
        <FieldLabel htmlFor="field-o-newsletter">Newsletter</FieldLabel>
        <Checkbox id="field-o-newsletter" />
      </Field>
      <Field orientation="responsive">
        <FieldContent>
          <FieldLabel htmlFor="field-o-bio">Bio</FieldLabel>
          <FieldDescription>Shown on your public profile.</FieldDescription>
        </FieldContent>
        <Input id="field-o-bio" placeholder="A short bio" />
      </Field>
    </FieldGroup>
  );
}

This example also needs: Checkbox.

Legend variants

Caption a FieldSet with a FieldLegend in the legend or smaller label size.

Notifications

How should we reach you?

Smaller caption
'use client';

import { Checkbox } from '@/components/nxds/components/checkbox/checkbox';
import {
  Field,
  FieldDescription,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from '@/components/nxds/components/field/field';

export default function FieldLegendVariants() {
  return (
    <div className="nx:flex nx:w-80 nx:flex-col nx:gap-6">
      <FieldSet>
        <FieldLegend variant="legend">Notifications</FieldLegend>
        <FieldDescription>How should we reach you?</FieldDescription>
        <Field orientation="horizontal">
          <FieldLabel htmlFor="field-l-email">Email</FieldLabel>
          <Checkbox id="field-l-email" defaultChecked />
        </Field>
      </FieldSet>
      <FieldSet>
        <FieldLegend variant="label">Smaller caption</FieldLegend>
        <Field orientation="horizontal">
          <FieldLabel htmlFor="field-l-sms">SMS</FieldLabel>
          <Checkbox id="field-l-sms" />
        </Field>
      </FieldSet>
    </div>
  );
}

This example also needs: Checkbox.

With error

Set data-invalid on the Field and aria-invalid on the control, then link the FieldError with aria-errormessage.

Use a memorable passphrase.

'use client';

import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldWithError() {
  return (
    <div className="nx:w-80">
      <Field data-invalid="true">
        <FieldLabel htmlFor="field-err">Password</FieldLabel>
        <Input
          id="field-err"
          type="password"
          aria-describedby="field-err-help"
          aria-errormessage="field-err-message"
          aria-invalid
        />
        <FieldDescription id="field-err-help">
          Use a memorable passphrase.
        </FieldDescription>
        <FieldError
          id="field-err-message"
          errors={[{ message: 'Must be at least 8 characters.' }]}
        />
      </Field>
    </div>
  );
}

With separator

Split alternative fields with a FieldSeparator that shows centered text.

OR
'use client';

import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
} from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldWithSeparator() {
  return (
    <div className="nx:w-80 nx:rounded-lg nx:border nx:border-border-default nx:bg-container nx:p-6">
      <FieldGroup>
        <Field>
          <FieldLabel htmlFor="field-sep-email">Email</FieldLabel>
          <Input id="field-sep-email" type="email" />
        </Field>
        <FieldSeparator>OR</FieldSeparator>
        <Field>
          <FieldLabel htmlFor="field-sep-phone">Phone</FieldLabel>
          <Input id="field-sep-phone" type="tel" />
        </Field>
      </FieldGroup>
    </div>
  );
}

Required and optional indicators

Mark a required field with FieldRequiredIndicator, or an optional one with its fallback text.

'use client';

import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldRequiredIndicator,
} from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldRequiredAndOptionalIndicators() {
  return (
    <FieldGroup className="nx:w-80">
      <Field>
        <FieldLabel htmlFor="field-required-email">
          Work email <FieldRequiredIndicator />
        </FieldLabel>
        <Input id="field-required-email" type="email" required />
      </Field>
      <Field>
        <FieldLabel htmlFor="field-optional-po">
          Purchase order{' '}
          <FieldRequiredIndicator fallback="Optional">
            Required
          </FieldRequiredIndicator>
        </FieldLabel>
        <Input id="field-optional-po" />
      </Field>
    </FieldGroup>
  );
}

Disabled

Set data-disabled on the Field and disabled on the control to dim the label.

This field is locked.

'use client';

import { Field, FieldDescription, FieldLabel } from '@/components/nxds/components/field/field';
import { Input } from '@/components/nxds/components/input/input';

export default function FieldDisabled() {
  return (
    <div className="nx:w-80">
      <Field data-disabled="true">
        <FieldLabel htmlFor="field-disabled">Email</FieldLabel>
        <Input id="field-disabled" type="email" disabled />
        <FieldDescription>This field is locked.</FieldDescription>
      </Field>
    </div>
  );
}

Props

Field

Field props
PropTypeDefaultDescription
orientation"horizontal" | "vertical" | "responsive" | null"vertical"No description

FieldContent

FieldContent adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldDescription

FieldDescription adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldError

FieldError props
PropTypeDefaultDescription
errorsFieldErrorsNo default documented

Validation errors to show. Duplicate messages show once, and several render as a list. Ignored when the error has content of its own.

FieldGroup

FieldGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldLabel

FieldLabel adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldLegend

FieldLegend props
PropTypeDefaultDescription
variant"legend" | "label""legend"

Caption size. legend is larger, and label matches a field label.

FieldRequiredIndicator

FieldRequiredIndicator props
PropTypeDefaultDescription
fallbackReactNodeNo default documented

Text for an optional field, shown instead of the *. Screen readers read it, and it overrides children.

FieldSeparator

FieldSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldSet

FieldSet adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldTitle

FieldTitle adds no props of its own; it accepts the props of the element or primitive it wraps.

Field lays out a label, control, description and error as one form field. Use it for any labeled form control. For a group of related fields, wrap them in FieldSet or FieldGroup.

<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <div class="nx:w-80">
    <Field>
      <FieldLabel for="field-email">Email</FieldLabel>
      <Input id="field-email" type="email" placeholder="you@example.com" />
      <FieldDescription>We never share it.</FieldDescription>
    </Field>
  </div>
</template>

This example also needs: Input.

Installation

  1. Install these components first:

    Label, Separator.

  2. Install the following dependencies:

    npm install class-variance-authority@^0.7.1
    
  3. Copy and paste the following code into your project.

    components/field/Field.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    import { type FieldVariants, fieldVariants } from '.';
    
    export interface FieldProps {
      orientation?: FieldVariants['orientation'];
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<FieldProps>(), {
      orientation: 'vertical',
    });
    </script>
    
    <template>
      <div
        role="group"
        data-slot="field"
        :data-orientation="orientation"
        :class="cn(fieldVariants({ orientation }), props.class)"
      >
        <slot />
      </div>
    </template>
    
    components/field/FieldContent.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldContentProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldContentProps>();
    </script>
    
    <template>
      <div
        data-slot="field-content"
        :class="
          cn(
            'nx:group/field-content nx:flex nx:flex-1 nx:flex-col nx:gap-1.5',
            props.class
          )
        "
      >
        <slot />
      </div>
    </template>
    
    components/field/FieldDescription.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldDescriptionProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldDescriptionProps>();
    </script>
    
    <template>
      <p
        data-slot="field-description"
        :class="
          cn(
            'nx:m-0 nx:typography-body-default nx:text-muted-foreground nx:group-has-data-[orientation=horizontal]/field:text-balance',
            'nx:[&>a]:text-inherit nx:[&>a]:underline nx:[&>a]:underline-offset-4 nx:[&>a:hover]:text-primary-subtle-foreground',
            props.class
          )
        "
      >
        <slot />
      </p>
    </template>
    
    components/field/FieldError.vue
    <script setup lang="ts">
    import { computed, type HTMLAttributes, type VNode } from 'vue';
    
    import { renderedNodes } from '../../lib/slots';
    import { cn } from '../../lib/utils';
    
    import type { FieldErrors } from '.';
    
    /** Unique, non-empty error messages in first-seen order. */
    function dedupeErrorMessages(errors: FieldErrors | undefined): string[] {
      const seen = new Map<string, string>();
      for (const error of errors ?? []) {
        if (error?.message) seen.set(error.message, error.message);
      }
      return [...seen.values()];
    }
    
    export interface FieldErrorProps {
      /**
       * Validation errors to show. Duplicate messages show once, and several render
       * as a list. Ignored when the error has content of its own.
       */
      errors?: FieldErrors;
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldErrorProps>();
    
    const slots = defineSlots<{ default?: () => VNode[] }>();
    
    const messages = computed(() => dedupeErrorMessages(props.errors));
    
    function hasChildren() {
      return renderedNodes(slots.default?.()).length > 0;
    }
    </script>
    
    <template>
      <div
        v-if="messages.length > 0 || hasChildren()"
        role="alert"
        aria-atomic="true"
        data-slot="field-error"
        :class="
          cn(
            'nx:typography-body-default nx:text-error-subtle-foreground',
            props.class
          )
        "
      >
        <slot>
          <template v-if="messages.length === 1">{{ messages[0] }}</template>
          <ul
            v-else
            class="nx:my-0 nx:ml-4 nx:flex nx:list-disc nx:flex-col nx:gap-1 nx:pl-0"
          >
            <li v-for="message in messages" :key="message">{{ message }}</li>
          </ul>
        </slot>
      </div>
    </template>
    
    components/field/FieldGroup.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldGroupProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldGroupProps>();
    </script>
    
    <template>
      <div
        data-slot="field-group"
        :class="
          cn(
            'nx:group/field-group nx:@container/field-group nx:flex nx:w-full nx:flex-col nx:gap-7 nx:data-[slot=checkbox-group]:gap-3 nx:*:data-[slot=field-group]:gap-4',
            props.class
          )
        "
      >
        <slot />
      </div>
    </template>
    
    components/field/FieldLabel.vue
    <script setup lang="ts">
    import { reactiveOmit } from '@vueuse/core';
    import type { LabelProps as RekaLabelProps } from 'reka-ui';
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    import { Label } from '../label';
    
    export interface FieldLabelProps extends RekaLabelProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldLabelProps>();
    
    const delegated = reactiveOmit(props, 'class');
    </script>
    
    <template>
      <Label
        v-bind="delegated"
        data-slot="field-label"
        :class="
          cn(
            'nx:group/field-label nx:peer/field-label nx:flex nx:w-fit nx:gap-2 nx:group-data-[disabled=true]/field:text-disabled-foreground',
            'nx:has-[>[data-slot=field]]:w-full nx:has-[>[data-slot=field]]:flex-col nx:has-[>[data-slot=field]]:rounded-md nx:has-[>[data-slot=field]]:border-default nx:*:data-[slot=field]:p-4',
            'nx:has-data-[state=checked]:border-primary-border nx:has-data-[state=checked]:bg-primary-subtle',
            props.class
          )
        "
      >
        <slot />
      </Label>
    </template>
    
    components/field/FieldLegend.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldLegendProps {
      /**
       * Caption size. `legend` is larger, and `label` matches a field label.
       */
      variant?: 'legend' | 'label';
      class?: HTMLAttributes['class'];
    }
    
    const props = withDefaults(defineProps<FieldLegendProps>(), {
      variant: 'legend',
    });
    </script>
    
    <template>
      <legend
        data-slot="field-legend"
        :data-variant="variant"
        :class="
          cn(
            'nx:mb-3 nx:p-0 nx:data-[variant=legend]:typography-heading-xsmall nx:data-[variant=label]:typography-label-default',
            props.class
          )
        "
      >
        <slot />
      </legend>
    </template>
    
    components/field/FieldRequiredIndicator.vue
    <script setup lang="ts">
    import type { HTMLAttributes, VNode } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldRequiredIndicatorProps {
      class?: HTMLAttributes['class'];
    }
    
    defineOptions({ inheritAttrs: false });
    
    const props = defineProps<FieldRequiredIndicatorProps>();
    
    const slots = defineSlots<{
      default?: () => VNode[];
      /** Text for an optional field, shown instead of the `*`. Screen readers read it, and it overrides the default slot. */
      fallback?: () => VNode[];
    }>();
    </script>
    
    <template>
      <span
        v-bind="$attrs"
        :aria-hidden="slots.fallback ? undefined : true"
        data-slot="field-required-indicator"
        :data-optional="slots.fallback ? 'true' : undefined"
        :class="
          cn(
            'nx:text-error-subtle-foreground nx:data-[optional=true]:typography-body-default nx:data-[optional=true]:text-muted-foreground',
            props.class
          )
        "
      >
        <slot v-if="slots.fallback" name="fallback" />
        <slot v-else>*</slot>
      </span>
    </template>
    
    components/field/FieldSeparator.vue
    <script setup lang="ts">
    import type { HTMLAttributes, VNode } from 'vue';
    
    import { renderedNodes } from '../../lib/slots';
    import { cn } from '../../lib/utils';
    import { Separator } from '../separator';
    
    export interface FieldSeparatorProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldSeparatorProps>();
    
    const slots = defineSlots<{ default?: () => VNode[] }>();
    
    function hasContent() {
      return renderedNodes(slots.default?.()).length > 0;
    }
    </script>
    
    <template>
      <div
        data-slot="field-separator"
        :data-content="hasContent()"
        :class="
          cn('nx:relative nx:-my-2 nx:h-5 nx:typography-body-default', props.class)
        "
      >
        <Separator class="nx:absolute nx:inset-0 nx:top-1/2" />
        <span
          v-if="hasContent()"
          data-slot="field-separator-content"
          class="nx:relative nx:mx-auto nx:block nx:w-fit nx:bg-container nx:px-2 nx:text-muted-foreground"
        >
          <slot />
        </span>
      </div>
    </template>
    
    components/field/FieldSet.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldSetProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldSetProps>();
    </script>
    
    <template>
      <fieldset
        data-slot="field-set"
        :class="
          cn(
            'nx:m-0 nx:flex nx:min-w-0 nx:flex-col nx:gap-6 nx:border-0 nx:p-0 nx:has-[>[data-slot=checkbox-group]]:gap-3 nx:has-[>[data-slot=radio-group]]:gap-3',
            props.class
          )
        "
      >
        <slot />
      </fieldset>
    </template>
    
    components/field/FieldTitle.vue
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue';
    
    import { cn } from '../../lib/utils';
    
    export interface FieldTitleProps {
      class?: HTMLAttributes['class'];
    }
    
    const props = defineProps<FieldTitleProps>();
    </script>
    
    <template>
      <div
        data-slot="field-label"
        :class="
          cn(
            'nx:flex nx:w-fit nx:items-center nx:gap-2 nx:typography-label-default nx:group-data-[disabled=true]/field:text-disabled-foreground',
            props.class
          )
        "
      >
        <slot />
      </div>
    </template>
    
    components/field/index.ts
    import { cva, type VariantProps } from 'class-variance-authority';
    
    export { default as Field, type FieldProps } from './Field.vue';
    export {
      default as FieldContent,
      type FieldContentProps,
    } from './FieldContent.vue';
    export {
      default as FieldDescription,
      type FieldDescriptionProps,
    } from './FieldDescription.vue';
    export { default as FieldError, type FieldErrorProps } from './FieldError.vue';
    export { default as FieldGroup, type FieldGroupProps } from './FieldGroup.vue';
    export { default as FieldLabel, type FieldLabelProps } from './FieldLabel.vue';
    export {
      default as FieldLegend,
      type FieldLegendProps,
    } from './FieldLegend.vue';
    export {
      default as FieldRequiredIndicator,
      type FieldRequiredIndicatorProps,
    } from './FieldRequiredIndicator.vue';
    export {
      default as FieldSeparator,
      type FieldSeparatorProps,
    } from './FieldSeparator.vue';
    export { default as FieldSet, type FieldSetProps } from './FieldSet.vue';
    export { default as FieldTitle, type FieldTitleProps } from './FieldTitle.vue';
    
    export const fieldVariants = cva(
      'nx:group/field nx:flex nx:w-full nx:gap-2 nx:data-[invalid=true]:text-error-subtle-foreground',
      {
        variants: {
          orientation: {
            vertical: 'nx:flex-col nx:*:w-full nx:[&>.sr-only]:w-auto',
            horizontal:
              'nx:flex-row nx:items-center nx:*:data-[slot=field-label]:flex-auto nx:has-[>[data-slot=field-content]]:items-start nx:has-[>[data-slot=field-content]]:[&>[role=checkbox]]:mt-px nx:has-[>[data-slot=field-content]]:[&>[role=radio]]:mt-px',
            responsive:
              'nx:flex-col nx:@md/field-group:flex-row nx:@md/field-group:items-center nx:*:w-full nx:@md/field-group:*:w-auto nx:[&>.sr-only]:w-auto nx:@md/field-group:*:data-[slot=field-label]:flex-auto nx:@md/field-group:has-[>[data-slot=field-content]]:items-start nx:@md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox]]:mt-px nx:@md/field-group:has-[>[data-slot=field-content]]:[&>[role=radio]]:mt-px',
          },
        },
        defaultVariants: {
          orientation: 'vertical',
        },
      }
    );
    
    export type FieldVariants = VariantProps<typeof fieldVariants>;
    
    /** Validation errors for a field, in the shape form libraries report them. */
    export type FieldErrors = Array<{ message?: string } | undefined>;
    
    lib/slots.ts
    import { Comment, Fragment, Text, type VNode } from 'vue';
    
    /** The nodes a slot renders, without comments and whitespace-only text. */
    export function renderedNodes(nodes: VNode[] | undefined) {
      return (nodes ?? []).filter(
        (node) =>
          node.type !== Comment &&
          !(node.type === Text && !String(node.children).trim())
      );
    }
    
    /** {@link renderedNodes} with each `v-for` or `<template>` fragment unwrapped into its children. */
    export function flattenedNodes(nodes: VNode[] | undefined): VNode[] {
      return renderedNodes(nodes).flatMap((node) =>
        node.type === Fragment && Array.isArray(node.children)
          ? flattenedNodes(node.children as VNode[])
          : [node]
      );
    }
    
  4. Keep each file at the path shown above it.

    Paths are relative to packages/vue/src/. Keep the same folders under your @/ root, and the imports between files work without edits.

Usage

import { Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldRequiredIndicator, FieldSeparator, FieldSet } from '@/components/field';

Examples

Orientations

Set orientation to vertical, horizontal or responsive, which follows the FieldGroup's width.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldGroup,
  FieldLabel,
} from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <FieldGroup class="nx:w-80">
    <Field orientation="vertical">
      <FieldLabel for="field-o-name">Name</FieldLabel>
      <Input id="field-o-name" placeholder="Ada Lovelace" />
    </Field>
    <Field orientation="horizontal">
      <FieldLabel for="field-o-newsletter">Newsletter</FieldLabel>
      <Checkbox id="field-o-newsletter" />
    </Field>
    <Field orientation="responsive">
      <FieldContent>
        <FieldLabel for="field-o-bio">Bio</FieldLabel>
        <FieldDescription>Shown on your public profile.</FieldDescription>
      </FieldContent>
      <Input id="field-o-bio" placeholder="A short bio" />
    </Field>
  </FieldGroup>
</template>

This example also needs: Checkbox.

Legend variants

Caption a FieldSet with a FieldLegend in the legend or smaller label size.

<script setup lang="ts">
import { Checkbox } from '@/components/checkbox';
import {
  Field,
  FieldDescription,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from '@/components/field';
</script>

<template>
  <div class="nx:flex nx:w-80 nx:flex-col nx:gap-6">
    <FieldSet>
      <FieldLegend variant="legend">Notifications</FieldLegend>
      <FieldDescription>How should we reach you?</FieldDescription>
      <Field orientation="horizontal">
        <FieldLabel for="field-l-email">Email</FieldLabel>
        <Checkbox id="field-l-email" :default-value="true" />
      </Field>
    </FieldSet>
    <FieldSet>
      <FieldLegend variant="label">Smaller caption</FieldLegend>
      <Field orientation="horizontal">
        <FieldLabel for="field-l-sms">SMS</FieldLabel>
        <Checkbox id="field-l-sms" />
      </Field>
    </FieldSet>
  </div>
</template>

This example also needs: Checkbox.

With error

Set data-invalid on the Field and aria-invalid on the control, then link the FieldError with aria-errormessage.

<script setup lang="ts">
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <div class="nx:w-80">
    <Field data-invalid="true">
      <FieldLabel for="field-err">Password</FieldLabel>
      <Input
        id="field-err"
        type="password"
        aria-describedby="field-err-help"
        aria-errormessage="field-err-message"
        aria-invalid="true"
      />
      <FieldDescription id="field-err-help"
        >Use a memorable passphrase.</FieldDescription
      >
      <FieldError
        id="field-err-message"
        :errors="[{ message: 'Must be at least 8 characters.' }]"
      />
    </Field>
  </div>
</template>

With separator

Split alternative fields with a FieldSeparator that shows centered text.

<script setup lang="ts">
import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldSeparator,
} from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <div
    class="nx:w-80 nx:rounded-lg nx:border nx:border-border-default nx:bg-container nx:p-6"
  >
    <FieldGroup>
      <Field>
        <FieldLabel for="field-sep-email">Email</FieldLabel>
        <Input id="field-sep-email" type="email" />
      </Field>
      <FieldSeparator>OR</FieldSeparator>
      <Field>
        <FieldLabel for="field-sep-phone">Phone</FieldLabel>
        <Input id="field-sep-phone" type="tel" />
      </Field>
    </FieldGroup>
  </div>
</template>

Required and optional indicators

Mark a required field with FieldRequiredIndicator, or an optional one with its #fallback text.

<script setup lang="ts">
import {
  Field,
  FieldGroup,
  FieldLabel,
  FieldRequiredIndicator,
} from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <FieldGroup class="nx:w-80">
    <Field>
      <FieldLabel for="field-required-email"
        >Work email <FieldRequiredIndicator
      /></FieldLabel>
      <Input id="field-required-email" type="email" required />
    </Field>
    <Field>
      <FieldLabel for="field-optional-po"
        >Purchase order
        <FieldRequiredIndicator>
          Required
          <template #fallback>Optional</template>
        </FieldRequiredIndicator>
      </FieldLabel>
      <Input id="field-optional-po" />
    </Field>
  </FieldGroup>
</template>

Disabled

Set data-disabled on the Field and disabled on the control to dim the label.

<script setup lang="ts">
import { Field, FieldDescription, FieldLabel } from '@/components/field';
import { Input } from '@/components/input';
</script>

<template>
  <div class="nx:w-80">
    <Field data-disabled="true">
      <FieldLabel for="field-disabled">Email</FieldLabel>
      <Input id="field-disabled" type="email" disabled />
      <FieldDescription>This field is locked.</FieldDescription>
    </Field>
  </div>
</template>

Props

Field

Field props
PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "responsive" | null"vertical"No description

FieldContent

FieldContent adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldDescription

FieldDescription adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldError

FieldError props
PropTypeDefaultDescription
errorsFieldErrorsNo default documented

Validation errors to show. Duplicate messages show once, and several render as a list. Ignored when the error has content of its own.

FieldGroup

FieldGroup adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldLabel

FieldLabel adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldLegend

FieldLegend props
PropTypeDefaultDescription
variant"label" | "legend""legend"

Caption size. legend is larger, and label matches a field label.

FieldRequiredIndicator

FieldRequiredIndicator adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldSeparator

FieldSeparator adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldSet

FieldSet adds no props of its own; it accepts the props of the element or primitive it wraps.

FieldTitle

FieldTitle adds no props of its own; it accepts the props of the element or primitive it wraps.