Meowland docs
Workbench

Component reference

Field

A labelled form control with optional help text and an error message.

Preview

Field has no story of its own; this one draws it as part of Input.

Read-only, errors and long content

Read-only content can still be selected and copied; disabled content cannot be edited. Explicit errors, composed fields and long content keep a clear association.

packages/meowtail/stories/input-states.stories.tsxline 21tsx
render: () => (
  <div className="workshop-composition-columns">
    <section className="workshop-stack">
      <Field
        label="Project path (read-only)"
        htmlFor="readonly-path"
        description="Can be selected and copied; it cannot be changed here."
      >
        <Input
          id="readonly-path"
          readonly
          value="/workspace/meowland/packages/meowtail/components/long-project-reference"
        />
      </Field>
      <Field label="Search files" htmlFor="search-files">
        <Input id="search-files" type="search" placeholder="Enter a file name…" />
      </Field>
      <Field
        label="Notification email"
        htmlFor="invalid-email"
        invalid
        error="Enter a complete email address."
      >
        <Input id="invalid-email" type="email" value="meow@" />
      </Field>
      <Field label="Access passphrase (disabled)" htmlFor="disabled-password">
        <Input id="disabled-password" type="password" value="demo-only" disabled />
      </Field>
      <Field
        label="Check range"
        description="For composed fields, the caller associates the label and description separately."
        descriptionId="range-description"
      >
        <div className="workshop-foundation-range">
          <label>
            From{" "}
            <Input
              aria-label="Start line"
              aria-describedby="range-description"
              type="number"
              value="1"
              min="1"
            />
          </label>
          <label>
            To{" "}
            <Input
              aria-label="End line"
              aria-describedby="range-description"
              type="number"
              value="80"
              min="1"
            />
          </label>
        </div>
      </Field>
    </section>
    <section className="workshop-stack">
      <Field label="Delivery notes (editable)" htmlFor="long-notes">
        <Textarea id="long-notes" rows={6}>
          First, confirm every component stays readable on narrow screens. Second, keep user input
          while switching themes. Third, open a dialog with the keyboard, cancel it and return.
          Long content should wrap naturally; never drop the evidence for checks to keep the
          screen tidy.
        </Textarea>
      </Field>
      <Field label="Last check record (read-only)" htmlFor="readonly-notes">
        <Textarea id="readonly-notes" rows={3} readonly>
          The record is kept for checking. Read-only does not mean inaccessible.
        </Textarea>
      </Field>
      <Field label="Waiting for the connection to return" htmlFor="disabled-notes">
        <Textarea id="disabled-notes" rows={3} disabled>
          Continue editing once the connection returns.
        </Textarea>
      </Field>
    </section>
  </div>
)

Installation

bun add meowtail

Usage

Taken from the examples on this page.

usage.tsxtsx
import { Field } from "meowtail";

<Field
  label="Project path (read-only)"
  htmlFor="readonly-path"
  description="Can be selected and copied; it cannot be changed here."
>
  <Input
    id="readonly-path"
    readonly
    value="/workspace/meowland/packages/meowtail/components/long-project-reference"
  />
</Field>

Examples

Other stories that draw Field.

Manual entry and host browsing

packages/meowtail/stories/path-picker.stories.tsxline 12tsx
render: () => {
  const value = signal("/Users/demo/Projects/my-project");
  let result: "selected" | "cancelled" | "failed" = "selected";
  return (
    <Stack gap="normal" className="workshop-constrained">
      <Field label="Project name" htmlFor="picker-name">
        <Input id="picker-name" value="Personal website" />
      </Field>
      <Field
        label="Local directory"
        htmlFor="picker-directory"
        description="The demo picker only returns a fixed path; it does not read files or grant access."
      >
        <PathPicker
          id="picker-directory"
          label="Local directory"
          value={value}
          kind="directory"
          onValueChange={(next) => {
            value.value = next;
          }}
          onChoose={async () => {
            await new Promise((resolve) => setTimeout(resolve, 450));
            if (result === "failed") throw new Error("Picker unavailable");
            return result === "cancelled" ? null : "/Users/demo/Projects/selected-project";
          }}
        />
      </Field>
      <Stack gap="compact" align="start">
        <Button
          variant="quiet"
          onClick={() => {
            result = "selected";
          }}
        >
          Simulate a successful choice
        </Button>
        <Button
          variant="quiet"
          onClick={() => {
            result = "cancelled";
          }}
        >
          Simulate a canceled choice
        </Button>
        <Button
          variant="quiet"
          onClick={() => {
            result = "failed";
          }}
        >
          Simulate a failed choice
        </Button>
      </Stack>
      <Field label="File path" htmlFor="picker-file">
        <PathPicker
          id="picker-file"
          label="File path"
          value="/tmp/report.md"
          onValueChange={() => {}}
          onChoose={async () => null}
          readOnly
        />
      </Field>
      <Field label="Waiting to save" htmlFor="picker-disabled">
        <PathPicker
          id="picker-disabled"
          label="Waiting to save"
          value="/tmp/project"
          kind="directory"
          onValueChange={() => {}}
          onChoose={async () => null}
          disabled
        />
      </Field>
      <Field
        label="Enter a directory manually"
        htmlFor="picker-manual"
        description="When the host offers no picker, keep entering an absolute path manually."
      >
        <PathPicker
          id="picker-manual"
          label="Enter a directory manually"
          value={value}
          onValueChange={(next) => {
            value.value = next;
          }}
        />
      </Field>
    </Stack>
  );
}

Legend and whole-group disabled

Uses native fieldset/legend for fields in one group; when the group is unavailable, inputs and selects follow form semantics together.

packages/meowtail/stories/foundations.stories.tsxline 29tsx
render: () => (
  <Measure width="narrow">
    <form>
      <Stack gap="relaxed">
        <FieldGroup
          legend="Project notifications"
          description="Choose how to receive alerts when work completes."
          descriptionId="notification-help"
        >
          <Field label="Delivery address" htmlFor="notification-email">
            <Input id="notification-email" type="email" placeholder="you@example.com" />
          </Field>
          <Field label="Alert frequency" htmlFor="notification-frequency">
            <Select id="notification-frequency">
              <option>Only when attention is needed</option>
              <option>Every time work completes</option>
            </Select>
          </Field>
        </FieldGroup>
        <Separator />
        <FieldGroup
          legend="Organization-managed settings"
          description="The organization manages this group; it cannot be changed now."
          disabled
        >
          <Field label="Organization name" htmlFor="managed-name">
            <Input id="managed-name" value="Meowland" />
          </Field>
          <Field label="Visibility" htmlFor="managed-visibility">
            <Select id="managed-visibility">
              <option>Project members only</option>
            </Select>
          </Field>
        </FieldGroup>
      </Stack>
    </form>
  </Measure>
)

API reference

Props

PropTypeDefault
labelRequired
Child–
description
Child–
error
Child–
htmlFor
string–
descriptionId
string–
errorId
string–
invalidExplicit state takes precedence over the live error message.
MaybeSignal<boolean>–

Also takes className, children and the attributes and events of its root element.

DOM parts

rootlabeldescriptionerrorcontrol

Registry

The component is registered as field in the meowtail component registry.