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.
Playground
Change a prop and watch it. The code below follows along.
Live
Open full cardInstall 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.
| Token | Tier | Declared as |
|---|---|---|
--dt-input-error-color | component | var(--dt-text-danger) |
--dt-input-hint-color | component | var(--dt-text-secondary) |
--dt-input-label-gap | component | var(--dt-space-stack-2xs) |
--dt-text-body-xs-family | semantic | var(--dt-font-family-sans) |
--dt-text-body-xs-line | semantic | var(--dt-line-height-xs) |
--dt-text-body-xs-size | semantic | var(--dt-font-size-xs) |
--dt-text-danger | semantic | var(--dt-color-red-800) |
--dt-text-label-md-family | semantic | var(--dt-font-family-secondary) |
--dt-text-label-md-line | semantic | var(--dt-line-height-sm) |
--dt-text-label-md-size | semantic | var(--dt-font-size-sm) |
--dt-text-label-md-weight | semantic | var(--dt-font-weight-medium) |
--dt-text-primary | semantic | var(--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.