macbetter

Text and fields

Text Field

Enter a single line of text, like a name, an address or a URL.

import { TextField } from "macbetter/text-field"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, width: "100%", maxWidth: 280 }}>
      <TextField size="sm" aria-label="Tag" placeholder="Tag" />
      <TextField aria-label="Board name" defaultValue="Q4 Launch Plan" />
      <TextField size="lg" aria-label="Title" placeholder="Untitled" />
    </div>
  )
}

When to use it

Use a text field for a short value someone types. Give it a visible label through a Field, or an aria-label when the context already names it, like a toolbar. Use the placeholder for an example or a hint ("name@example.com"), never as the only label: it disappears as soon as someone types.

For a password use a Secure Field, for search a Search Field, and for several lines a Text Area.

Usage

import {
  TextField,
  TextFieldAdornment,
  TextFieldClear,
  TextFieldGroup,
} from "macbetter/text-field"

TextField is a native input, so type, placeholder, autoComplete, inputMode and the rest pass straight through. size is sm, md or lg: 22, 28 and 36 points tall on macOS, 34, 44 and 52 on iOS.

Adornments and a clear button

Wrap the field in a TextFieldGroup to put text or an icon beside it, or a clear button after it. The group draws the field; pressing an adornment focuses the input. TextFieldClear is the xmark.circle.fill button: it appears only while there's text, empties the field and puts focus back.

https://
GB
import {
  TextField,
  TextFieldAdornment,
  TextFieldClear,
  TextFieldGroup,
} from "macbetter/text-field"
import { AtSignIcon } from "lucide-react"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, width: "100%", maxWidth: 300 }}>
      <TextFieldGroup>
        <TextFieldAdornment>https://</TextFieldAdornment>
        <TextField aria-label="Website" defaultValue="macbetter.dev" />
        <TextFieldClear aria-label="Clear website" />
      </TextFieldGroup>
      <TextFieldGroup>
        <TextFieldAdornment>
          <AtSignIcon aria-hidden="true" />
        </TextFieldAdornment>
        <TextField aria-label="Username" placeholder="username" autoComplete="username" />
      </TextFieldGroup>
      <TextFieldGroup>
        <TextField aria-label="Storage limit" inputMode="numeric" defaultValue="250" />
        <TextFieldAdornment>GB</TextFieldAdornment>
      </TextFieldGroup>
    </div>
  )
}

Variants

variant="auto" follows the platform. bordered is the macOS field, white with a hairline edge. filled is the iOS gray field. plain has no chrome at all, for a field inside a list row or a table cell, where the row is the field.

import { TextField } from "macbetter/text-field"

export default function Example() {
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 12, width: "100%", maxWidth: 300 }}>
      <TextField variant="bordered" aria-label="Bordered" placeholder="Bordered" />
      <TextField variant="filled" aria-label="Filled" placeholder="Filled" />
      <TextField variant="plain" aria-label="Plain" placeholder="Plain, for a list row" />
      <TextField aria-label="Disabled" defaultValue="Managed by your organization" disabled />
    </div>
  )
}

Platform behavior

  • macOS. A rounded rectangle on the field background, with a hairline edge and a faint inner shadow. The accent focus halo appears the moment the field starts editing, by mouse or keyboard, and settles in from just outside the edge. Text is 13 points.
  • iPadOS and iOS. A gray filled field with 10-point corners and 17-point text, 44 points tall. The caret is the accent color. A plain field shows only its caret when editing, as in Settings.
  • Every platform. An invalid field turns its edge and halo red. With Increase Contrast on, the filled field gains an edge. The clear button keeps a full pointer or touch target even though it draws at 14 or 17 points.

Accessibility

  • The input is native, so text editing, selection, autofill and spell checking work as everywhere else.
  • Name every field with a Field label or aria-label. Name TextFieldClear when "Clear text" isn't specific enough, like "Clear website".
  • Inside a Field, an invalid input gets aria-invalid and is described by its error.

API

TextField

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"Height, padding and type size, from the platform's metrics.
variant"auto" | "bordered" | "filled" | "plain""auto"Bordered on macOS and filled on touch, or a fixed look.
valuestring—The text, for a controlled field.
defaultValuestring—The text it starts with. A form reset returns here.
onValueChange(value, details) => void—Runs on typing, a clear press, a reset or a controller. details.reason says which.

TextFieldGroup

PropTypeDefaultDescription
size"sm" | "md" | "lg""md"The size of the field the group draws.
variant"auto" | "bordered" | "filled" | "plain""auto"The look of the field the group draws.

TextFieldClear

A button, named "Clear text" unless you pass aria-label. It draws xmark.circle.fill unless it has children.

TextFieldAdornment takes the props of a span.

Data attributes

AttributeOnValues
data-sizeTextField, TextFieldGroupsm, md, lg
data-variantTextField, TextFieldGroupauto, bordered, filled, plain
data-filledTextField, group, clearPresent while there's text
data-invalidTextFieldPresent inside an invalid Field
data-disabled, data-readonlyTextField, groupPresent when disabled or read-only

CSS variables

VariablemacOSiOSWhat it sets
--mb-text-field-radius6px10pxCorner radius at md
--mb-text-field-padding8px12pxInline padding at md
--mb-text-field-font13px17pxType at md
--mb-text-field-icon14px17pxAdornment icon and clear button size
--mb-text-field-auto-bgField whiteTertiary fillWhat auto fills with
--mb-field-placeholderInk at 64% (60% dark)SamePlaceholder color for every text control, kept at 4.5:1