Field

The wrapper that gives a control its label, hint, and error. Input, Textarea, and Select render their own Field when you pass them a label; use Field directly when you need to wrap something else.

Forms Field.jsx Field.d.ts Field.md

Playground

Change a prop and watch it. The code below follows along.

Install and import

Field is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Field } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

The wrapper that gives a control its label, hint, and error. Input, Textarea, and Select render their own Field when you pass them a label; use Field directly when you need to wrap something else.

Use it when

  • Wrapping a custom or composed control that needs the standard label and error treatment.

Don't use it when

  • You are using Input, Textarea, or Select with a label prop. They already build one.

Example

``jsx <Field label="Timezone" hint="Used for scheduling" htmlFor="tz"> <MyCustomPicker id="tz" /> </Field> ``

Accessibility

htmlFor must point at the control's id. The error renders in role="alert" so it is announced when it appears. The required asterisk is aria-hidden, so set required on the control so assistive tech hears it once, not twice.

For a control that isn't a single input, such as a group of checkboxes, htmlFor has nothing to point at. Pass labelId and messageId instead, then point the group's aria-labelledby at the label and its aria-describedby at the hint or error. CheckboxGroup and RadioGroup do this for you.

Content

Labels are sentence case nouns without a colon. Hints explain the format or the consequence. Errors say what happened and what to do next: "That email is already in use. Try signing in instead."

Props

The typed contract, from Field.d.ts.

import * as React from "react";

/** Label, control, hint, and error wrapper. Every form control in a form goes inside one. */
export interface FieldProps extends React.HTMLAttributes<HTMLDivElement> {
  label?: string;
  /** Helper text shown below the control. */
  hint?: string;
  /** Replaces the hint and renders in an alert live region. */
  error?: string;
  /** Adds a required marker to the label. Also set the required attribute on the control. */
  required?: boolean;
  /** id of the control this labels. */
  htmlFor?: string;
  /** id for the label element, so a control that isn't a single input (a group) can point aria-labelledby at it. */
  labelId?: string;
  /** id for the hint or error line, so a control can point aria-describedby at it. */
  messageId?: string;
  children?: React.ReactNode;
}

export declare function Field(props: FieldProps): React.JSX.Element;

Tokens it reads

Every custom property Field.jsx resolves, read from the source: 3 component, 9 semantic. Component tier first, because that is the one to reach for.

  • component Its own tier. Override one of these and only this component moves.
  • semantic Shared roles. Override one and everything using that role moves with it.
TokenTierDeclared as
--dt-input-error-colorcomponentvar(--dt-text-danger)
--dt-input-hint-colorcomponentvar(--dt-text-secondary)
--dt-input-label-gapcomponentvar(--dt-space-stack-2xs)
--dt-text-body-xs-familysemanticvar(--dt-font-family-sans)
--dt-text-body-xs-linesemanticvar(--dt-line-height-xs)
--dt-text-body-xs-sizesemanticvar(--dt-font-size-xs)
--dt-text-dangersemanticvar(--dt-color-red-800)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-linesemanticvar(--dt-line-height-sm)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-label-md-weightsemanticvar(--dt-font-weight-medium)
--dt-text-primarysemanticvar(--dt-color-neutral-900)

Source

React only, no dependencies, every value a token.

import React from "react";

export function Field({ label, hint, error, required = false, htmlFor, labelId, messageId, children, style, ...rest }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--dt-input-label-gap)", ...style }} {...rest}>
      {label && (
        <label
          htmlFor={htmlFor}
          id={labelId}
          style={{
            fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)",
            lineHeight: "var(--dt-text-label-md-line)", fontWeight: "var(--dt-text-label-md-weight)",
            color: "var(--dt-text-primary)",
          }}
        >
          {label}
          {required && <span aria-hidden="true" style={{ color: "var(--dt-text-danger)", marginLeft: 2 }}>*</span>}
        </label>
      )}
      {children}
      {(error || hint) && (
        <div
          id={messageId}
          role={error ? "alert" : undefined}
          style={{
            fontFamily: "var(--dt-text-body-xs-family)", fontSize: "var(--dt-text-body-xs-size)",
            lineHeight: "var(--dt-text-body-xs-line)",
            color: error ? "var(--dt-input-error-color)" : "var(--dt-input-hint-color)",
          }}
        >
          {error || hint}
        </div>
      )}
    </div>
  );
}

In context

Field also appears on the forms reference cards, alongside the rest of its family.