Meowland docs
Workbench

Component reference

FieldGroup

A titled set of related fields, as a native fieldset.

Preview

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>
)

Installation

bun add meowtail

Usage

Taken from the examples on this page.

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

<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>

Examples

The other stories that show this component.

Delivery preferences

Edit a local draft, submit an invalid email, correct it, and save. The field error and saved feedback stay next to the controls and action they describe.

packages/meowtail/stories/settings-workflow.stories.tsxline 15tsx
function deliveryPreferences() {
  const displayName = signal("Alex Morgan");
  const email = signal("alex.morgan@northstar.studio");
  const frequency = signal("daily");
  const mentions = signal(true);
  const milestones = signal(true);
  const emailError = signal<string | undefined>(undefined);
  const feedback = signal("");
  const snapshot = () => ({
    displayName: displayName.value,
    email: email.value,
    frequency: frequency.value,
    mentions: mentions.value,
    milestones: milestones.value,
  });
  const saved = signal(snapshot());
  const dirty = derived(() => JSON.stringify(snapshot()) !== JSON.stringify(saved.value));
  const status = derived(
    () =>
      feedback.value || (dirty.value ? "Unsaved changes" : "Changes apply to this preview only."),
  );
  let emailInput: HTMLInputElement | undefined;
  const edited = () => {
    feedback.value = "";
  };

  return (
    <Container width="narrow" as="main">
      <Stack gap="relaxed">
        <PageHeader
          title="Delivery preferences"
          description="Choose where project updates arrive and which ones deserve your attention."
        />
        <form
          noValidate
          aria-label="Delivery preferences"
          onSubmit={(event: Event) => {
            event.preventDefault();
            if (!emailInput?.validity.valid) {
              emailError.value = "Enter a valid email address, such as alex@northstar.studio.";
              feedback.value = "";
              // Field flushes its live message and ARIA before focus moves to the input.
              queueMicrotask(() => {
                if (!emailInput?.isConnected) return;
                emailInput.focus();
                feedback.value = "Preferences were not saved. Check the email address above.";
              });
              return;
            }
            emailError.value = undefined;
            saved.value = snapshot();
            feedback.value = "Delivery preferences saved for this preview.";
          }}
        >
          <Stack gap="relaxed">
            <FieldGroup
              legend="Account"
              description="Your name appears on the project updates you share with teammates."
            >
              <Field label="Full name" htmlFor="delivery-name">
                <Input
                  id="delivery-name"
                  name="displayName"
                  autoComplete="name"
                  value={displayName}
                  onInput={(event: Event) => {
                    displayName.value = (event.currentTarget as HTMLInputElement).value;
                    edited();
                  }}
                />
              </Field>
            </FieldGroup>

            <FieldGroup
              legend="Delivery"
              description="Send project updates to the inbox you check during your working day."
            >
              <Field
                label="Email address"
                htmlFor="delivery-email"
                description="Used for project updates and your scheduled digest."
                error={emailError}
              >
                <Input
                  id="delivery-email"
                  name="email"
                  type="email"
                  autoComplete="email"
                  required
                  value={email}
                  ref={(input: HTMLInputElement) => {
                    emailInput = input;
                  }}
                  onInput={(event: Event) => {
                    const input = event.currentTarget as HTMLInputElement;
                    email.value = input.value;
                    if (input.validity.valid) emailError.value = undefined;
                    edited();
                  }}
                />
              </Field>
              <Field
                label="Project digest"
                htmlFor="delivery-frequency"
                description="A summary of progress, decisions, and upcoming milestones."
              >
                <Select
                  id="delivery-frequency"
                  name="frequency"
                  value={frequency}
                  onChange={(event: Event) => {
                    frequency.value = (event.currentTarget as HTMLSelectElement).value;
                    edited();
                  }}
                >
                  <option value="daily">Every weekday at 9:00 AM</option>
                  <option value="weekly">Every Monday at 9:00 AM</option>
                  <option value="off">Do not send a project digest</option>
                </Select>
              </Field>
            </FieldGroup>

            <FieldGroup
              legend="Project notifications"
              description="These updates arrive as they happen, separately from your digest."
            >
              <Checkbox
                name="mentions"
                label="When someone mentions me or requests my review"
                checked={mentions}
                onChange={(event: Event) => {
                  mentions.value = (event.currentTarget as HTMLInputElement).checked;
                  edited();
                }}
              />
              <Checkbox
                name="milestones"
                label="When a project milestone is completed or its due date changes"
                checked={milestones}
                onChange={(event: Event) => {
                  milestones.value = (event.currentTarget as HTMLInputElement).checked;
                  edited();
                }}
              />
            </FieldGroup>

            <Stack gap="compact" align="start">
              <Button type="submit">Save preferences</Button>
              <span role="status" aria-live="polite" aria-atomic="true">
                {status}
              </span>
            </Stack>
          </Stack>
        </form>
      </Stack>
    </Container>
  );
}

API reference

Props

PropTypeDefault
legendRequired
Child–
description
Child–
descriptionId
string–
disabled
boolean–

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

DOM parts

rootlegenddescriptioncontent

Registry

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