Skip to content
design
Components

MMSI input

A field for a nine-digit MMSI that reads in groups of three and names the station it identifies.

On this page
Nine digits, from the ship radio licence.
The vessel's own MMSI with numbers the crew meet on the air: a ship and its flag, a handheld, a coast station the field refuses, a SAR aircraft, an EPIRB and a buoy.
Code
import { Button } from "@bosunkit/components/button";
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The vessel's own MMSI, required, with numbers the crew meet on the air: press one to fill the field and read what " +
  "it identifies, from a ship and its flag to a coast station the field refuses and a beacon in the water.";

interface Sample {
  readonly name: string;
  readonly mmsi: string;
}

const SAMPLES: readonly Sample[] = [
  { name: "UK ship", mmsi: "235012345" },
  { name: "Dutch handheld", mmsi: "824412345" },
  { name: "Coast station", mmsi: "002320011" },
  { name: "SAR aircraft", mmsi: "111232500" },
  { name: "EPIRB", mmsi: "974321098" },
  { name: "Buoy", mmsi: "992351234" },
];

export default function Showcase(): ReactElement {
  const [mmsi, setMMSI]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ width: "100%", maxWidth: 420, display: "grid", gap: "var(--space-md)" }}>
      <MmsiInput
        label="MMSI"
        required
        value={mmsi}
        onValueChange={setMMSI}
        hint="Nine digits, from the ship radio licence."
      />
      <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-xs)" }}>
        {SAMPLES.map((sample: Sample): ReactElement => (
          <Button
            key={sample.mmsi}
            size="sm"
            variant="ghost"
            onClick={(): void => {
              setMMSI(sample.mmsi);
            }}
          >
            {sample.name}
          </Button>
        ))}
      </div>
    </div>
  );
}

An MMSI input takes a Maritime Mobile Service Identity on a numeric keypad and shows it in groups of three (235 012 345), as a radio's label writes it. It keeps the digits of whatever is typed or pasted, at most nine, and drops the trailing pad of the ten-digit form NMEA 2000 carries. The page receives the nine digits without spaces.

The field takes what bosund's MMSI validator (core/mmsi) takes: nine digits, the first not 0. A number that starts with 0 belongs to a coast station or a group call; the field refuses it as soon as the ninth digit is in. A number short of nine digits is named once the crew leave the field. Under a whole number the field says what it identifies, by its leading digits as ITU-R M.585 allocates them: a ship station and the flag of its MID, a handheld VHF with DSC, a SAR aircraft, a craft from a parent ship, an aid to navigation, or a survival device (an AIS-SART, a man-overboard device, an EPIRB). Country names come from CLDR in the crew's language.

Examples

Empty

Nine digits, from the ship radio licence.
An empty field opens a numeric keypad on a phone; the hint says where to find the number.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string =
  "An empty field opens a numeric keypad on a phone; the hint says where to find the number.";

export default function Empty(): ReactElement {
  return <MmsiInput label="MMSI" hint="Nine digits, from the ship radio licence." />;
}

Ship station

Ship station, United Kingdom
A ship station's MMSI reads in groups of three, and the hint names the flag its MID belongs to.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string =
  "A ship station's MMSI reads in groups of three, and the hint names the flag its MID belongs to.";

export default function ShipStation(): ReactElement {
  return <MmsiInput label="MMSI" defaultValue="235012345" />;
}

Every kind of station

Handheld VHF with DSC, Netherlands
SAR aircraft, United Kingdom
Craft from a parent ship, United Kingdom
Aid to navigation, United Kingdom
AIS-SART, a search and rescue transmitter
Man-overboard device
EPIRB with AIS
Ship station, MID 306
Every kind of number the field names: a handheld VHF with DSC, a SAR aircraft, a craft from a parent ship, an aid to navigation, an AIS-SART, a man-overboard device and an EPIRB, and a MID shared by three territories.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string =
  "Every kind of number the field names: a handheld VHF with DSC, a SAR aircraft, a craft from a parent ship, an " +
  "aid to navigation, an AIS-SART, a man-overboard device and an EPIRB, and a MID shared by three territories.";

const NUMBERS: readonly string[] = [
  "824412345",
  "111232500",
  "982351234",
  "992351234",
  "970321098",
  "972321098",
  "974321098",
  "306123456",
];

export default function Kinds(): ReactElement {
  return (
    <div
      style={{
        width: "100%",
        display: "grid",
        gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
        gap: "var(--space-md)",
      }}
    >
      {NUMBERS.map((mmsi: string): ReactElement => (
        <MmsiInput key={mmsi} aria-label={`MMSI ${mmsi}`} defaultValue={mmsi} readOnly />
      ))}
    </div>
  );
}

Invalid

A number that starts with 0 belongs to a coast station or a group call, which bosund refuses as an MMSI; the field says so as soon as the ninth digit is in. A short number is called short once the crew leave the field.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string =
  "A number that starts with 0 belongs to a coast station or a group call, which bosund refuses as an MMSI; the " +
  "field says so as soon as the ninth digit is in. A short number is called short once the crew leave the field.";

export default function Invalid(): ReactElement {
  return <MmsiInput label="MMSI" defaultValue="002320011" />;
}

Pasted or typed text

The page receives: no MMSI yet
Paste or type anything: the field keeps the digits, drops the pad of the ten-digit NMEA 2000 form, and reports the nine digits without spaces.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Paste or type anything: the field keeps the digits, drops the pad of the ten-digit NMEA 2000 form, and reports the " +
  "nine digits without spaces.";

export default function Pasted(): ReactElement {
  const [mmsi, setMMSI]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ width: "100%", maxWidth: 360, display: "grid", gap: "var(--space-sm)" }}>
      <MmsiInput label="MMSI" value={mmsi} onValueChange={setMMSI} />
      <output style={{ fontSize: 12, color: "var(--text-dim)" }}>
        The page receives: {mmsi === "" ? "no MMSI yet" : mmsi}
      </output>
    </div>
  );
}

Read-only

Ship station, Netherlands
The vessel's own MMSI, shown from the configuration and not changed here.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string = "The vessel's own MMSI, shown from the configuration and not changed here.";

export default function ReadOnly(): ReactElement {
  return <MmsiInput label="Own MMSI" defaultValue="244123456" readOnly />;
}

Disabled

Craft from a parent ship, United Kingdom
A disabled field, with its number still grouped.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string = "A disabled field, with its number still grouped.";

export default function Disabled(): ReactElement {
  return <MmsiInput label="Tender MMSI" defaultValue="982351234" disabled />;
}

Inside a form field

Ship station, Germany
Inside a form field, with no label of its own: the form field names it and the MMSI field still names the station.
Code
import { FormField } from "@bosunkit/components/form-field";
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import type { ReactElement } from "react";

export const description: string =
  "Inside a form field, with no label of its own: the form field names it and the MMSI field still names the station.";

export default function InFormField(): ReactElement {
  return (
    <FormField label="Buddy boat">
      <MmsiInput aria-label="Buddy boat MMSI" defaultValue="211234567" />
    </FormField>
  );
}

Typed and pasted spaces

The page receives: no MMSI yet
Type the number with spaces, as it is read out (235 012 345), or paste it with hyphens, dots or non-breaking spaces: the separators are ignored, a space typed at a group's space moves past it, and the nine digits are the value.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-input";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Type the number with spaces, as it is read out (235 012 345), or paste it with hyphens, dots or non-breaking " +
  "spaces: the separators are ignored, a space typed at a group's space moves past it, and the nine digits are " +
  "the value.";

export default function Spaces(): ReactElement {
  const [mmsi, setMMSI]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");
  return (
    <div style={{ width: "100%", maxWidth: 360, display: "grid", gap: "var(--space-sm)" }}>
      <MmsiInput label="MMSI" value={mmsi} onValueChange={setMMSI} />
      <output style={{ fontSize: 12, color: "var(--text-dim)" }}>
        The page receives: {mmsi === "" ? "no MMSI yet" : mmsi}
      </output>
    </div>
  );
}

Paste

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

  • nine digits, alone or in groups split by spaces, non-breaking spaces, hyphens or dots;
  • a number with a label or other text around it (MMSI: 235 012 345), passing over other numbers such as a phone number; where several nine-digit numbers are in the text, the one labeled MMSI is taken, and with no label the first;
  • the ten-digit NMEA 2000 form written whole, without its trailing pad; ten digits in groups are a phone number.

The paste button shows only when the clipboard holds something the field can take, named by what it would paste (Paste 235 012 345). 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.

  • MMSI: 235 012 345
  • 235-012-345
  • Althea, MXYZ7, tel +44 7700 900123, MMSI 235012345
  • 2441234560
  • 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 MMSI appears; Ctrl+V works everywhere. The field finds the MMSI among the other text, and the last line holds none, so no button appears for it.
Code
import { MmsiInput } from "@bosunkit/components/mmsi-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 MMSI appears; Ctrl+V works everywhere. The field finds the MMSI among the other text, and the last line holds none, so no button appears for it.";

const SAMPLES: readonly string[] = [
  "MMSI: 235 012 345",
  "235-012-345",
  "Althea, MXYZ7, tel +44 7700 900123, MMSI 235012345",
  "2441234560",
  "Hello",
];

export default function Paste(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 520, display: "grid", gap: "var(--space-md)" }}>
      <MmsiInput label="MMSI" />
      <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 MMSI's digits, no spaces (235012345); "" for none.
defaultValuestring | undefined–The MMSI it starts with when the caller does not hold value.
onValueChange((value: string) => void) | undefined–Called with the digits as the crew type or paste, at most nine.
hintstring | undefined–Text under the field while the number says nothing yet, translated by the caller.
errorstring | undefined–The caller's own error, shown in place of the field's.
showIdentityboolean | undefinedtrueWhether to show what the number identifies (a ship and its country, a coast station, a device). On by default.
requiredboolean | undefined–Whether the field must hold an MMSI.
disabledboolean | undefined–Whether the field takes no input.
readOnlyboolean | undefined–Whether the field shows its number without letting the crew change it.
idstring | undefined–The input's id, for a Label's htmlFor.
namestring | undefined–The form field name; the form receives the digits without spaces.
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.
aria-describedbystring | undefined–Further ids that describe the input.
classNamestring | undefined–Classes added to the field's root.
testIdstring | undefined–data-testid on the field.
refRef<HTMLInputElement> | undefined–The input element.
motionMotionProp | undefined–How the error line moves; off by default, as FormField's.