Skip to content
design
Components

Outlined field

The outlined box with the field's name on its top edge that the maritime inputs draw in.

On this page
A vessel's registration in outlined fields: a required name, a call sign with its error, a read-only home port, a disabled tonnage, an icon and a help button inside the outline, and a field in a form field.
Code
import { FormField } from "@bosunkit/components/form-field";
import { OutlinedField, OutlinedInput } from "@bosunkit/components/outlined-field";
import { CircleHelp, Ship } from "lucide-react";
import { useState, type ChangeEvent, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A vessel's registration: a required name the crew fill, a call sign that is wrong until it has four to seven " +
  "letters and digits, a read-only home port, a disabled tonnage, a field with an icon at the start and a help " +
  "button at the end that shows its help, and the same input inside a form field.";

function callSignError(callSign: string): string | undefined {
  return callSign === "" || /^[A-Z0-9]{4,7}$/.test(callSign)
    ? undefined
    : "A call sign has 4 to 7 letters and digits, such as MXYZ7. Check it on the ship radio licence.";
}

export default function Showcase(): ReactElement {
  const [name, setName]: [string, Dispatch<SetStateAction<string>>] = useState<string>("Althea");
  const [callSign, setCallSign]: [string, Dispatch<SetStateAction<string>>] = useState<string>("MX7");
  const [helpShown, setHelpShown]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 560,
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
        gap: "var(--space-lg)",
      }}
    >
      <OutlinedInput
        label="Vessel name"
        required
        value={name}
        onChange={(event: ChangeEvent<HTMLInputElement>): void => {
          setName(event.target.value);
        }}
      />
      <OutlinedInput
        label="Call sign"
        hint="Four to seven letters and digits."
        value={callSign}
        error={callSignError(callSign)}
        autoCapitalize="characters"
        onChange={(event: ChangeEvent<HTMLInputElement>): void => {
          setCallSign(event.target.value.toUpperCase());
        }}
      />
      <OutlinedInput label="Home port" defaultValue="Falmouth" readOnly />
      <OutlinedInput label="Gross tonnage" defaultValue="18" disabled />
      <OutlinedField
        label="Hull identification number"
        labelFor="showcase-hin"
        filled
        start={<Ship size={18} aria-hidden="true" />}
        end={
          <button
            type="button"
            className="outlined-field__icon-button"
            aria-label="Help"
            aria-expanded={helpShown}
            onClick={(): void => {
              setHelpShown(!helpShown);
            }}
          >
            <CircleHelp size={20} aria-hidden="true" />
          </button>
        }
        hint={helpShown ? "Fourteen characters, moulded into the transom on the starboard side." : undefined}
        hintId="showcase-hin-help"
      >
        <input
          id="showcase-hin"
          className="outlined-field__control"
          defaultValue="GB-HAN46012K526"
          aria-describedby={helpShown ? "showcase-hin-help" : undefined}
        />
      </OutlinedField>
      <FormField label="Owner">
        <OutlinedInput aria-label="Owner" defaultValue="Mara Quist" />
      </FormField>
    </div>
  );
}

An outlined field is a box with the field's name in a gap on its top edge. The maritime inputs (MMSI, position, date of birth, calendar, email, phone and the crew picker) all draw in it, so a form built from them reads as one family. OutlinedInput is a text input in the box; OutlinedField is the box alone, around one control or a group of them: a select's trigger, a combo box's input, a text area, a time.

Every field of a form draws outlined: the schema form draws each of its fields so, and a form built by hand does the same, so a form reads as one family and its rows lose the line a label above each box takes. A switch has no box and stands with its name beside it. A plain input is for a single field outside a form: a search, a filter above a table, a setting saved on its own, a value in a toolbar.

The label rests in the middle of an empty field and moves onto the edge when the field holds a value or the focus, by a transform on the motion tokens, and at once under reduced motion. A required field shows a red asterisk the accessible name leaves out. Under the box sits a hint, or the error in its place: the outline and the label turn red, the control is marked aria-invalid, and the error is a role="alert" the control is described by. The box is as high as an input beside it: 48 px, the touch target the kit sets, on a phone or a tablet (a screen up to the 68.75em breakpoint) or under a coarse pointer, and 44 px on a desktop with a fine pointer. Without a label the box names nothing, so the input takes aria-label or sits in a form field.

Under the box, status shows a quiet line in the hint's place, read out politely (role="status"): the maritime inputs say there what a paste took or why it took nothing. Each of them shows a paste button first among its trailing icons when the clipboard holds a value it can take. The trailing icons sit close together at the right edge, each with a 48 px target reaching out on its outer side.

The label and field body share the field's fill in every state, read-only and disabled included: the same background the plain input beside it draws, so a field on a page, card or panel reads as an input on that surface, and the floating label is centered in the outline's gap.

Examples

Empty

An empty field: the label rests inside the outline until the field takes focus, then moves onto its edge and the placeholder shows.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string =
  "An empty field: the label rests inside the outline until the field takes focus, then moves onto its edge and " +
  "the placeholder shows.";

export default function Empty(): ReactElement {
  return <OutlinedInput label="Given names" placeholder="e.g. MARIA LOPEZ" />;
}

Filled

A field with a value: the label sits in a gap in the outline's top edge.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string = "A field with a value: the label sits in a gap in the outline's top edge.";

export default function Filled(): ReactElement {
  return <OutlinedInput label="Family name" defaultValue="Quist" />;
}

Required

A required field: a red asterisk after the label, which the accessible name leaves out, and `required` on the input.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string =
  "A required field: a red asterisk after the label, which the accessible name leaves out, and `required` on the input.";

export default function Required(): ReactElement {
  return <OutlinedInput label="Given names" required />;
}

Invalid

An error turns the outline and label red, marks the input `aria-invalid` and is announced; it takes the hint's place.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string =
  "An error turns the outline and label red, marks the input `aria-invalid` and is announced; it takes the hint's place.";

export default function Invalid(): ReactElement {
  return (
    <OutlinedInput
      label="Passport number"
      defaultValue="X12"
      hint="As printed on the passport's photo page."
      error="This passport number is too short. Check it against the photo page."
    />
  );
}

Read-only

A read-only field draws a dashed outline; the value can be selected and copied.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string = "A read-only field draws a dashed outline; the value can be selected and copied.";

export default function ReadOnly(): ReactElement {
  return <OutlinedInput label="Home port" defaultValue="Falmouth" readOnly />;
}

Disabled

A disabled field is dimmed and takes no focus.
Code
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string = "A disabled field is dimmed and takes no focus.";

export default function Disabled(): ReactElement {
  return <OutlinedInput label="Gross tonnage" defaultValue="18" disabled />;
}

Inside a form field

Inside a form field, the field draws no label of its own and takes its name from `aria-label`; the form field carries the error.
Code
import { FormField } from "@bosunkit/components/form-field";
import { OutlinedInput } from "@bosunkit/components/outlined-field";
import type { ReactElement } from "react";

export const description: string =
  "Inside a form field, the field draws no label of its own and takes its name from `aria-label`; the form field " +
  "carries the error.";

export default function InFormField(): ReactElement {
  return (
    <FormField label="Owner" error="Enter the owner's name as on the registration.">
      <OutlinedInput aria-label="Owner" aria-invalid />
    </FormField>
  );
}

Props

OutlinedInput

A text input in the outlined box. It takes every attribute of a native input as well.

PropTypeDefaultDescription
classNamestring | undefined–Additional control classes.
labelstring | undefined–The field's name, drawn on the outline's top edge.
hintReactNode–Text under the field that helps fill it, translated by the caller.
errorstring | undefined–What is wrong with the value and how to fix it. Sets aria-invalid and is announced when it appears.
statusFieldStatus | undefined–A quiet line in the hint's place; see OutlinedField.
startReactNode–Content before the input inside the outline.
endReactNode–Content after the input inside the outline.
fieldClassNamestring | undefined–Classes added to the field's root.
testIdstring | undefined–data-testid on the field.
refRef<HTMLInputElement> | undefined–The input element.
boxRefRefObject<HTMLDivElement | null> | undefined–The outlined box, for a popover to open under.
motionMotionProp | undefined–How the error line moves; see OutlinedField.
onValueChange((value: string, eventDetails: Input.ChangeEventDetails) => void) | undefined–Callback fired when the value changes. Use when controlled.
defaultValueField.Control.Props['defaultValue'] | undefined–The default value of the input. Use when uncontrolled.
valueReact.ComponentProps<'input'>['value'] | undefined–The value of the input. Use when controlled.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.

Also takes every attribute of Omit<BaseInput.Props, "children" | "className">.

OutlinedField

The box alone. labelFor makes the label a <label> for one control; labelId names a group through aria-labelledby.

PropTypeDefaultDescription
labelstring | undefined–The field's name, drawn on the outline's top edge; omitted when a Label or FormField names the control.
labelForstring | undefined–The id of the control the label names: the label is then a <label for>.
labelIdstring | undefined–The label's own id, for a group of controls that names itself with aria-labelledby.
requiredboolean | undefined–Whether the field must be filled: a red asterisk after the label. Set required on the control as well.
invalidboolean | undefined–Whether the value is wrong: a red outline and label. The control sets aria-invalid itself.
disabledboolean | undefined–Whether the field takes no input: dimmed.
readOnlyboolean | undefined–Whether the field shows its value without letting the crew change it: a dashed outline.
filledboolean | undefined–Whether the field holds a value, so the label rests on the outline; it also rests there while focused.
hintReactNode–Text under the field that helps fill it, translated by the caller.
hintIdstring | undefined–The hint's id, for the control's aria-describedby.
errorstring | undefined–What is wrong with the value and how to fix it; replaces the hint and is announced when it appears.
errorIdstring | undefined–The error's id, for the control's aria-describedby.
statusFieldStatus | undefined–A quiet line in the hint's place, such as what a paste took or why it took nothing; read out politely (role="status"). The error, when there is one, shows in its place.
startReactNode–Content before the control inside the outline, such as an icon or a country code.
endReactNode–Content after the control inside the outline, such as a help or a calendar button.
childrenrequiredReactNode–The control or controls.
afterReactNode–Content under the outline, before its error or hint, such as the files a file input holds.
classNamestring | undefined–Classes added to the field's own.
testIdstring | undefined–data-testid on the field.
refRef<HTMLDivElement> | undefined–The field's root element.
boxRefRefObject<HTMLDivElement | null> | undefined–The outlined box, for a popover to open under.
motionMotionProp | undefined–How the error line moves. By default, as a FormField's, an error that appears rises in and fades out when the value is fixed; one there on the first render does not move. A preset or { enter, exit, change } chooses; false turns it off. The error is announced and readable from its first frame.
  • Form field: the label above an input, for forms in the kit's other style.
  • Input: the plain text input.
  • Accessibility: target sizes and accessible names.