Skip to content
design
Components

Email input

An email address field that offers the common mail domains once the @ is typed.

On this page
A crew member's email: after the @ the field offers the common mail domains and a sailing one, narrowing as the crew type.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A crew member's email, required: after the @ the field offers the common mail domains and a sailing one the page " +
  "adds, narrowing as the crew type; arrows and Enter take one, and an address without its domain is named once the " +
  "crew leave the field.";

const DOMAINS: readonly string[] = [
  "gmail.com",
  "hotmail.com",
  "yahoo.com",
  "outlook.com",
  "icloud.com",
  "sailmail.com",
];

export default function Showcase(): ReactElement {
  const [email, setEmail]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ width: "100%", maxWidth: 400, display: "grid", gap: "var(--space-sm)" }}>
      <EmailInput
        label="Email"
        required
        domains={DOMAINS}
        value={email}
        onValueChange={setEmail}
        placeholder="name@example.com"
        hint="For the crew list and the weather briefings."
      />
    </div>
  );
}

An email input is a type="email" field the browser can fill (autoComplete="email"), with a phone's email keyboard. Once the crew have typed a name and an @, it lists whole addresses for the mail domains in domains (gmail.com, hotmail.com, yahoo.com, outlook.com and icloud.com by default), narrowed to those that start with what follows the @. Arrow keys move through the list, Enter or Tab takes an address, Escape closes the list, and a press on one takes it without losing the focus.

The field takes the shape bosund's forms give an email field: a name, one @, and a domain with a dot inside it. When the crew leave the field with an address that lacks one of them, the error names the part to fix: a space to remove, the @, a second @, the name, or the domain.

Examples

Empty

An empty email field: the browser can fill it, and a phone shows its email keyboard.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string =
  "An empty email field: the browser can fill it, and a phone shows its email keyboard.";

export default function Empty(): ReactElement {
  return <EmailInput label="Email" />;
}

Domains after the @

Typed up to the @, the field lists the five common domains; press the field and type a letter to narrow them.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string =
  "Typed up to the @, the field lists the five common domains; press the field and type a letter to narrow them.";

export default function Suggestions(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 400 }}>
      <EmailInput label="Email" defaultValue="mara.quist@" />
    </div>
  );
}

Invalid

An address bosund's form pattern would refuse, here one without a domain: the error names the part that is missing.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string =
  "An address bosund's form pattern would refuse, here one without a domain: the error names the part that is " +
  "missing.";

export default function Invalid(): ReactElement {
  return (
    <EmailInput label="Email" defaultValue="mara.quist@gmail" error="Add the domain after the @, such as gmail.com." />
  );
}

Read-only

A read-only address offers no domains.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string = "A read-only address offers no domains.";

export default function ReadOnly(): ReactElement {
  return <EmailInput label="Owner's email" defaultValue="owner@example.org" readOnly />;
}

Disabled

A disabled email field.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string = "A disabled email field.";

export default function Disabled(): ReactElement {
  return <EmailInput label="Email" defaultValue="ines.varga@example.org" disabled />;
}

Inside a form field

Inside a form field, named by `aria-label`.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import { FormField } from "@bosunkit/components/form-field";
import type { ReactElement } from "react";

export const description: string = "Inside a form field, named by `aria-label`.";

export default function InFormField(): ReactElement {
  return (
    <FormField label="Marina office">
      <EmailInput aria-label="Marina office email" defaultValue="office@example.org" />
    </FormField>
  );
}

Paste

The paste button, at the end of the field, fills it with the address in the clipboard. It reads:

  • a mailto: link, without its subject or other query;
  • a name with the address in angle brackets (Mara Quist <mara.quist@example.org>);
  • the first address in a sentence.

The address it takes has the shape bosund's forms require. The paste button shows only when the clipboard holds something the field can take, named by what it would paste (Paste mara.quist@example.org). To know that, the field reads the clipboard when it takes focus and when the window comes back, and only where the browser lets the page read it without asking: the clipboard-read permission is granted. Where the browser would ask first, or does not know the permission (Firefox, Safari), the field reads nothing and shows no button, so no prompt appears on focus; Ctrl+V and a long press on a phone paste through the same reader. A read-only or disabled field shows no paste button.

  • mailto:mara.quist@example.org?subject=Crew%20list
  • Mara Quist <mara.quist@example.org>
  • Write to mara.quist@example.org before Friday.
  • Hello
Copy one of these lines, then focus the field: where the browser lets the page read the clipboard, a paste button named by the address appears; Ctrl+V works everywhere. The address comes out of a mailto link, a named address or a sentence.
Code
import { EmailInput } from "@bosunkit/components/email-input";
import type { ReactElement } from "react";

export const description: string =
  "Copy one of these lines, then focus the field: where the browser lets the page read the clipboard, a paste button named by the address appears; Ctrl+V works everywhere. The address comes out of a mailto link, a named address or a sentence.";

const SAMPLES: readonly string[] = [
  "mailto:mara.quist@example.org?subject=Crew%20list",
  "Mara Quist <mara.quist@example.org>",
  "Write to mara.quist@example.org before Friday.",
  "Hello",
];

export default function Paste(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 520, display: "grid", gap: "var(--space-md)" }}>
      <EmailInput label="Email" />
      <ul style={{ margin: 0, paddingLeft: "var(--space-lg)", fontFamily: "var(--font-mono)", fontSize: 12 }}>
        {SAMPLES.map((sample: string): ReactElement => (
          <li key={sample} style={{ userSelect: "all", overflowWrap: "anywhere" }}>
            {sample}
          </li>
        ))}
      </ul>
    </div>
  );
}

Props

PropTypeDefaultDescription
labelstring | undefined–The field's name on the outline, translated by the caller; omit it inside a FormField.
valuestring | undefined–The address as typed.
defaultValuestring | undefined–The address it starts with when the caller does not hold value.
onValueChange((value: string) => void) | undefined–Called with the text as the crew type it, and with the whole address when they choose a suggestion.
domainsreadonly string[] | undefinedEMAIL_DOMAINSThe domains offered after the @, in this order; the five common mail domains by default.
hintstring | undefined–Text under the field, translated by the caller.
errorstring | undefined–The caller's own error, shown in place of the field's.
requiredboolean | undefined–Whether the field must hold an address.
disabledboolean | undefined–Whether the field takes no input.
readOnlyboolean | undefined–Whether the field shows its address without letting the crew change it.
idstring | undefined–The input's id, for a Label's htmlFor.
namestring | undefined–The form field name.
placeholderstring | undefined–Text shown while the field is empty and focused, translated by the caller.
aria-labelstring | undefined–Names the input when there is no label.
classNamestring | undefined–Classes added to the field's root.
testIdstring | undefined–data-testid on the field.
refRef<HTMLInputElement> | undefined–The input element.
motionMotionProp | undefined–false opens the suggestion list at once and keeps the error line still; true also moves the error line. The list fades in by default, as the combo box's does.