Monet
@etched/monet 0.0.1

Field

Ported

Label, help text and error around one form control. The label and messages are linked to the control. Native <div> attributes and ref pass through.

Kumo source

The ID printed on the package.

import { Input } from "@etched/monet/input";
import { Field } from "@etched/monet/field";

export function Preview() {
  return (
    <Field label="Chip ID" description="The ID printed on the package.">
      <Input placeholder="SH-24A-0000" />
    </Field>
  );
}

Installation

import { Field, FieldGroup, Fieldset } from "@etched/monet/field";

Examples

Fields

The ID printed on the package, for example SH-24A-0193.

Lot L-7799 does not exist.

Type a value that is not an email, then leave the field.

import { Input } from "@etched/monet/input";
import { Field } from "@etched/monet/field";

export function Fields() {
  return (
    <div className="grid">
      <Field label="Chip ID" description="The ID printed on the package, for example SH-24A-0193.">
        <Input placeholder="SH-24A-0000" />
      </Field>
      <Field label="Notes" required={false}>
        <Input placeholder="Anything the next shift should know" />
      </Field>
      <Field label="Lot" error="Lot L-7799 does not exist.">
        <Input defaultValue="L-7799" />
      </Field>
      <Field label="Email" error={{ message: "Enter an email address.", match: "typeMismatch" }} description="Type a value that is not an email, then leave the field.">
        <Input type="email" defaultValue="sam@" />
      </Field>
    </div>
  );
}

Info button and several errors

labelTooltip puts an info button after the label; its text shows on hover and focus. A list of errors shows one per line; repeats show once.

  • Use at least 12 characters.
  • Add a number or a symbol.
import { Input } from "@etched/monet/input";

export function InfoButtonAndSeveralErrors() {
  return (
    <div className="grid">
      <Input label="Requisition ID" labelTooltip="From Greenhouse: Jobs, then the job's ID under its title." placeholder="REQ-0000" />
      <Input
        label="Password"
        type="password"
        defaultValue="etched"
        error={["Use at least 12 characters.", "Add a number or a symbol.", "Use at least 12 characters."]}
      />
    </div>
  );
}

Label beside the control

FieldGroup orientation="horizontal" puts each label in a column beside its control, also for controls that take label directly. Help text and errors go under the control. An <hr> between two fields draws a line.

Interview invites come from this address.


Time zone
import { Input, InputArea } from "@etched/monet/input";
import { Select } from "@etched/monet/select";
import { FieldGroup } from "@etched/monet/field";

const zones = { pt: "Pacific (PT)", et: "Eastern (ET)", tw: "Taipei (CST)" };

export function LabelBesideTheControl() {
  return (
    <FieldGroup orientation="horizontal" style={{ maxInlineSize: "40rem" }}>
      <Input label="Display name" defaultValue="Ana Ruiz" />
      <Input label="Work email" type="email" defaultValue="ana@etched.ai" description="Interview invites come from this address." />
      <hr />
      <Select label="Time zone" items={zones} defaultValue="pt" />
      <InputArea label="Email signature" labelTooltip="Plain text only. Links are kept." required={false} rows={3} placeholder="Shown under every outreach email" />
    </FieldGroup>
  );
}

Responsive

orientation="responsive" keeps the label beside the control while both fit, and puts it on top in a narrow box, such as a phone screen or a dialog. Drag the box's corner to try it.

Channel #bring-up is private. Invite the Orbit bot first.
import { Input } from "@etched/monet/input";
import { FieldGroup } from "@etched/monet/field";

export function Responsive() {
  return (
    <div className="card" style={{ boxSizing: "border-box", resize: "horizontal", inlineSize: "40rem", maxInlineSize: "100%", padding: "var(--et-space-4)" }}>
      <FieldGroup orientation="responsive">
        <Input label="Team name" defaultValue="Silicon bring-up" />
        <Input label="Slack channel" defaultValue="#bring-up" error="Channel #bring-up is private. Invite the Orbit bot first." />
      </FieldGroup>
    </div>
  );
}

Fieldset

One legend over several fields, with one message for the group. disabled turns off every control in it, for example while the form sends.

LinkedIn and/or email

Enter at least one, so we can reach the candidate.

Referrer

Enter a LinkedIn URL or an email.

import { Input } from "@etched/monet/input";
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Fieldset } from "@etched/monet/field";

export function Fieldsets() {
  const [sending, setSending] = useState(false);

  return (
    <div className="grid">
      <Fieldset legend="LinkedIn and/or email" description="Enter at least one, so we can reach the candidate.">
        <Input aria-label="LinkedIn URL" placeholder="linkedin.com/in/…" />
        <Input aria-label="Email" type="email" placeholder="name@example.com" />
      </Fieldset>
      <Fieldset legend="Referrer" error="Enter a LinkedIn URL or an email." disabled={sending}>
        <Input aria-label="Referrer LinkedIn URL" placeholder="linkedin.com/in/…" />
        <Input aria-label="Referrer email" type="email" placeholder="name@example.com" />
      </Fieldset>
      <div className="row">
        <Button onClick={() => setSending(!sending)}>{sending ? "Stop sending" : "Send"}</Button>
      </div>
    </div>
  );
}

Accessibility

  • Field links its label to the control, so the label is the control's name. required={false} adds "(optional)" to the label, and so to the name.
  • description and error describe the control (aria-describedby); an error sets aria-invalid. The error is not a live region: screen readers hear it with the control.
  • labelTooltip adds an info button named "More information" (labelTooltipLabel, on a Field or a control) and described by the tip. It sits beside the label, so the control's name stays clean.
  • Fieldset is a group, named by its legend and described by its description or error. disabled turns off every control in it.
  • Without label, give the control an aria-label; in a Fieldset, give each control its own name. Set nativeLabel={false} when the control in a Field is a button.

API reference

FieldGroup

PropTypeDefaultDescription
orientation"vertical" | "horizontal" | "responsive""vertical"Where every field inside puts its label, including the fields that controls make from label.

Field

PropTypeDefaultDescription
children *ReactNode–The control: Input, InputArea, Select and so on.
labelReactNode–Visible label. Leave it out when the control renders its own label.
labelTooltipReactNode–More about the field, behind an info button after the label. Shown on hover and focus; screen readers hear it as the button's description.
labelTooltipLabelstring"More information"Names the info button.
requiredboolean–false adds the optional marker after the label.
optionalLabelReactNode"(optional)"Text after the label when required is false.
descriptionReactNode–Help text below the control. Hidden while an error shows.
errorFieldError–
nativeLabelbooleantruefalse for button triggers (Select, Combobox): the label names the control without passing clicks or hover to it.
orientation"vertical" | "horizontal" | "responsive""vertical", or the orientation of the FieldGroup around itWhere the label goes.

Fieldset

PropTypeDefaultDescription
classNamestring–
legend *ReactNode–The group's title.
children *ReactNode–The fields: Field, Input, InputArea, Select and so on.
requiredboolean–false adds the optional marker after the legend.
descriptionReactNode–Help text below the group. Hidden while an error shows.
errorReactNode–A message for the whole group, for example "Enter a LinkedIn URL or an email".
disabledboolean–Turns off every control in the group, for example while the form sends.