macbetter

Text and fields

Field

Give a control a label, a description and an error, stacked above it or beside it.

Shown to people you share boards with.
import { Field, FieldDescription, FieldLabel } from "macbetter/field"
import { TextField } from "macbetter/text-field"

export default function Example() {
  return (
    <div style={{ width: "100%", maxWidth: 320 }}>
      <Field name="display-name">
        <FieldLabel>Display name</FieldLabel>
        <TextField defaultValue="Hollis Grant" autoComplete="name" />
        <FieldDescription>Shown to people you share boards with.</FieldDescription>
      </Field>
    </div>
  )
}

When to use it

Wrap every form control in a field. The field connects the label, the description and any error to the control for you, so a click on the label focuses it and a screen reader hears all three.

  • Stacked puts the label above the control. It's the default, and it suits narrow layouts, iOS forms and sign-in screens.
  • Horizontal puts the label beside the control, right-aligned in its own column, the way macOS forms, settings panes and inspectors do it. On macOS, end each label with a colon ("Name:").
  • Keep descriptions short and factual. Show an error only after someone tries to submit, and say how to fix it.

Usage

import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSet,
  Label,
} from "macbetter/field"

Side by side

Put fields in a FieldGroup with layout="horizontal" and every label lines up in one column, sized to the longest. A single field can take layout="horizontal" on its own. A field with layout="stacked" inside a horizontal group sits in the control column, for a checkbox or a note that needs no label of its own.

Stored in your keychain.
import { Field, FieldDescription, FieldGroup, FieldLabel } from "macbetter/field"
import { SecureField } from "macbetter/secure-field"
import { TextField } from "macbetter/text-field"

export default function Example() {
  return (
    <FieldGroup layout="horizontal" style={{ width: "100%", maxWidth: 420 }}>
      <Field name="account-name">
        <FieldLabel>Name:</FieldLabel>
        <TextField defaultValue="Hollis Grant" autoComplete="name" />
      </Field>
      <Field name="email">
        <FieldLabel>Email Address:</FieldLabel>
        <TextField type="email" placeholder="name@example.com" autoComplete="email" />
      </Field>
      <Field name="password">
        <FieldLabel>Password:</FieldLabel>
        <SecureField placeholder="Required" autoComplete="current-password" />
        <FieldDescription>Stored in your keychain.</FieldDescription>
      </Field>
    </FieldGroup>
  )
}

Validation

Native constraints like required and type="email" work as they do in HTML. Errors stay hidden until a submit, then follow the value as it changes. Give each FieldError a match to write your own message for one problem, or pass invalid to the field when your server decides.

"use client"

import { Button } from "macbetter/button"
import { Field, FieldError, FieldLabel } from "macbetter/field"
import { TextField } from "macbetter/text-field"

export default function Example() {
  return (
    <form
      style={{ display: "flex", flexDirection: "column", gap: 14, width: "100%", maxWidth: 320 }}
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="invite">
        <FieldLabel>Invite by email</FieldLabel>
        <TextField type="email" required placeholder="name@example.com" />
        <FieldError match="valueMissing">Enter an email address.</FieldError>
        <FieldError match="typeMismatch">Enter an email address like name@example.com.</FieldError>
      </Field>
      <Button type="submit" variant="filled" style={{ alignSelf: "flex-end" }}>
        Send Invite
      </Button>
    </form>
  )
}

Groups

FieldSet and FieldLegend group related fields under a heading and disable them together. Label names a control that isn't inside a field, pointed at it with htmlFor.

Platform behavior

  • macOS. Labels are 13-point regular text in the primary label color. Horizontal labels are right-aligned and centered on the control's first line; descriptions are 11-point secondary text under the control.
  • iPadOS and iOS. Stacked labels are footnote-sized secondary text, inset to line up with the text inside the field, like a grouped list's header. Horizontal labels are left-aligned, as in a Contacts edit row. Descriptions are footnotes.
  • Every platform. Error text uses the destructive color and stays 4.5:1 against the background in light and dark.

Accessibility

  • The label names the control through aria-labelledby and a native for, so clicking it focuses the control or toggles a checkbox.
  • The description and any shown error describe the control through aria-describedby; an invalid control gets aria-invalid.
  • After a failed submit, the first invalid control takes focus.

API

Field

PropTypeDefaultDescription
layout"stacked" | "horizontal""stacked"Label above the control, or beside it in its own column.
namestring—The name the control submits under, when it doesn't set its own.
disabledbooleanfalseDisables the control and marks every part.
invalidboolean—Shows the field as invalid now (true) or never (false). Leave it out to follow the browser's validation.

FieldGroup

PropTypeDefaultDescription
layout"stacked" | "horizontal""stacked"Horizontal lines every field's label up in one column.

FieldError

PropTypeDefaultDescription
matchboolean | "valueMissing" | "typeMismatch" | …—When the error shows: always, never, or only for one validity problem.

FieldLabel, FieldDescription, FieldSet, FieldLegend and Label take the props of their elements. FieldDescription renders a div.

Data attributes

AttributeOnValues
data-layoutField, FieldGroupstacked, horizontal
data-invalidEvery partPresent while the field shows as invalid
data-disabledEvery partPresent when the field is disabled
data-requiredEvery partPresent when the control is required

CSS variables

VariablemacOSiOSWhat it sets
--mb-field-gap6px7pxSpace between label, control and description
--mb-field-group-gap14px20pxSpace between fields in a group
--mb-field-column-gap8px16pxSpace between a horizontal label and its control
--mb-field-label-font13px regularFootnoteStacked label type
--mb-field-label-alignendstartHorizontal label alignment