Password input
A password field that password managers and the operating system recognize, with show and hide, a Caps Lock warning and the rules of a new password.
On this page
Connections
Code
import { Button } from "@bosunkit/components/button";
import { Card } from "@bosunkit/components/card";
import { FormField } from "@bosunkit/components/form-field";
import { Input } from "@bosunkit/components/input";
import { PasswordInput } from "@bosunkit/components/password-input";
import type { CSSProperties, ReactElement, SyntheticEvent } from "react";
export const description: string =
"The Starlink router's Wi-Fi network and the cloud assistant's key, as two forms in one card: a password manager " +
"saves the password under the network's name and leaves the key alone. Press Caps Lock in the password field to " +
"see the warning.";
/** The two forms side by side where the card has room for both, as on a phone on its side, one over the other below. */
const FORMS: CSSProperties = {
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 18em), 1fr))",
alignItems: "start",
gap: "var(--space-lg)",
};
function keepOnPage(event: SyntheticEvent<HTMLFormElement>): void {
event.preventDefault();
}
export default function Showcase(): ReactElement {
return (
<Card title="Connections" style={{ width: "100%", maxWidth: 820 }}>
<div style={FORMS}>
<form onSubmit={keepOnPage} style={{ display: "grid", gap: "var(--space-md)" }}>
<FormField label="Wi-Fi network">
<Input autoComplete="username" defaultValue="STARLINK-Althea" spellCheck={false} />
</FormField>
<FormField label="Wi-Fi password" hint="On the label under the Starlink router.">
<PasswordInput purpose="current" defaultValue="harbor-light-26" hideOnBlur />
</FormField>
<Button type="submit" variant="primary">
Join network
</Button>
</form>
<form onSubmit={keepOnPage} style={{ display: "grid", gap: "var(--space-md)" }}>
<FormField label="Anthropic API key" hint="From the Anthropic console. Paste it from your password manager.">
<PasswordInput purpose="secret" placeholder="sk-ant-…" />
</FormField>
<Button type="submit">Save key</Button>
</form>
</div>
</Card>
);
}
A password input is a native <input type="password">, so the browser, the operating system and password managers
treat it as a password: masked on screen, kept out of the keyboard's learned words, offered for saving and filling.
A button inside the field's right edge shows and hides the password, a line under it says when Caps Lock is on, and
a new password lists its rules. Paste always works.
bosun's own sign-in has no password: the crew sign in with an email link or a passkey. This field is for the passwords and keys of other systems: the Wi-Fi networks the vessel joins, the password the crew set for the vessel's own network, and the API keys and bot tokens of the services bosun talks to. A code sent for one use goes in a one-time code input, which shows it.
Examples
Current password
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
"A marina's Wi-Fi password, `purpose=\"current\"`: the form shows no username, so `username` puts the network's " +
"name in a hidden field and a password manager saves the password under it.";
export default function CurrentPassword(): ReactElement {
return (
<form style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput
appearance="outlined"
label="Password for Marina Falmouth Guest"
username="Marina Falmouth Guest"
purpose="current"
/>
</form>
);
}
New password with rules
The checklist marks each rule with a shape and a word, so it reads without color. A new password is asked for once: the show button lets the crew read what they typed, which a second field only checks by making them type it again (GOV.UK's research on password fields; web.dev, "Don't double up inputs").
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
'The vessel\'s own Wi-Fi password, `purpose="new"`: a checklist of its length and the names to leave out, and ' +
"`passwordRules` for the managers' generators. The show button lets the crew check what they typed, so there is " +
"no second field.";
/** WPA2's limits for a passphrase: 8 to 63 printable ASCII characters. Lengths and a character set, no classes. */
const RULES: string = "minlength: 8; maxlength: 63; allowed: ascii-printable;";
export default function NewPassword(): ReactElement {
return (
<form style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput
appearance="outlined"
label="New Wi-Fi password"
username="Althea"
purpose="new"
passwordRules={RULES}
minLength={8}
maxLength={63}
contextWords={["Althea"]}
/>
</form>
);
}
Secret
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
'A Telegram bot token, `purpose="secret"`: the managers\' extensions leave the field alone, and pasting works.';
export default function Secret(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput
appearance="outlined"
label="Telegram bot token"
hint="From BotFather in Telegram, as 123456789:AA…"
purpose="secret"
/>
</div>
);
}
Shared
A password the crew choose to hand on, such as the guest network's, is purpose="shared": a text input, so nothing
masks it, with no show button, and it stays shown when its form is submitted. It lists the rules of a new password
and writes the managers' opt-outs, since it is not a login of the crew's.
- At least 8 charactersdone
- Doesn't contain Althea or Althea Guestdone
- Not a common or leaked passwordchecking
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
'The guest network\'s password, `purpose="shared"`: guests are told it, so it is plain text with no show button, ' +
"never hidden, with the checklist of a new password.";
export default function Shared(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput
appearance="outlined"
label="Guest Wi-Fi password"
purpose="shared"
minLength={8}
maxLength={63}
contextWords={["Althea", "Althea Guest"]}
defaultValue="harbor-lantern-41"
/>
</div>
);
}
Invalid
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
"A password the network refused: the error says what happened and what to do, and is announced.";
export default function Invalid(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput
appearance="outlined"
label="Wi-Fi password"
username="Marina Falmouth Guest"
defaultValue="harborguest2"
error="The network refused this password. Check it with the marina office and type it again."
/>
</div>
);
}
Disabled
Code
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string = "A disabled field: it cannot be typed in or shown.";
export default function Disabled(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<PasswordInput appearance="outlined" label="Wi-Fi password" defaultValue="harbor-light-26" disabled />
</div>
);
}
Inside a form field
On the label under the router.
Code
import { FormField } from "@bosunkit/components/form-field";
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
"Inside a form field: the field's label names the input, and its hint and required state reach it.";
export default function InFormField(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360 }}>
<FormField label="Wi-Fi password" hint="On the label under the router." required>
<PasswordInput username="STARLINK-Althea" />
</FormField>
</div>
);
}
Plain and outlined
Code
import { FormField } from "@bosunkit/components/form-field";
import { PasswordInput } from "@bosunkit/components/password-input";
import type { ReactElement } from "react";
export const description: string =
"The two appearances: `plain`, the kit's text input inside a form field, and `outlined`, the maritime inputs' " +
"outlined field with its name on the outline. The button sits inside the field's edge in both.";
export default function Appearances(): ReactElement {
return (
<div style={{ width: "100%", maxWidth: 360, display: "grid", gap: "var(--space-lg)" }}>
<FormField label="Wi-Fi password">
<PasswordInput defaultValue="harbor-light-26" />
</FormField>
<PasswordInput appearance="outlined" label="Wi-Fi password" defaultValue="harbor-light-26" />
</div>
);
}
Showing the password
The button is named "Show password" or "Hide password" in the crew's language, points at the input with
aria-controls, and has a 48 px target. Its name says what pressing it does next, so it carries no aria-pressed: a
name that changes with a pressed state reads as "Hide password, pressed". A screen reader also hears "The password is
shown." or "The password is hidden." from a polite status.
A press keeps the focus, the caret and the selection in the field, and the input keeps its autocomplete while it
shows text. Submitting the form hides the password before the browser and the managers read the form; with
hideOnBlur it also hides when the focus leaves the field and its button. A shared password has no button and is
never hidden.
Caps Lock is read from every key the crew press in the field. While it is on, the line under the field says "Caps Lock is on." in a polite status that the input names in its description.
The rules of a new password
With purpose="new" or "shared" the checklist lists a minimum length (minLength), the names the password must
leave out (contextWords, such as the vessel's and the network's), and "Not a common or leaked password", then any
requirements of the caller's, and none of them is a mix of character kinds. NIST SP 800-63B-4, section 3.1.1.2, sets this: a verifier SHALL require a minimum length (8
characters when the password is one of several factors, 15 when it is the only one), SHALL compare a new password
with a list of common, expected and compromised ones, and SHALL NOT impose composition rules such as "a capital, a
small letter and a digit", which add little against guessing and turn away the long random passwords managers
generate. The Wi-Fi examples take 8, the shortest passphrase WPA2 allows, and give the generators
minlength: 8; maxlength: 63; allowed: ascii-printable;: the protocol's limits, with no required: classes. A
password that holds a listed name is refused with the name it found ("The password contains Althea. Leave it out
and choose another."), and one that is too short with its count ("The password has 6 characters. Use at least 8.");
checkNewPassword returns the refusal for a setting row's validate. Source:
NIST SP 800-63B-4, Authentication and Authenticator Management.
The common-password rule and the strength line come from zxcvbn-ts, which
estimates how many guesses a password would take from its lists of common and leaked passwords, common words, names
and keyboard patterns, in English and the crew's language, with contextWords as the first words an attacker tries.
A password estimated at fewer than 10⁶ guesses is refused as common or leaked: "password1", "sailboat99" and
"Althea2026" are, a random 8-character password (10⁸) and "harbor-lantern-41" (10¹²) are not. The strength line
fills one to four segments and says the level in words, "Strength: weak", "fair", "good" or "strong", so it reads
without color. The estimator and its dictionaries load when a new-password field first renders, never on a page
without one; until they arrive the rule says "checking", and a save is refused with a line that asks the crew to
try again in a moment.
Password managers
1Password, the browsers' own managers, LastPass and Bitwarden decide what a field is from its markup. The field writes the markup they read:
purpose | autocomplete | Also written | For |
|---|---|---|---|
current | current-password | A password the crew type to use something: a Wi-Fi network, a router | |
new | new-password | passwordrules from passwordRules; minLength and maxLength as given | A password the crew choose |
secret | off | data-1p-ignore, data-lpignore="true", data-bwignore, data-form-type="other" | An API key or bot token another service issued |
shared | off | The same opt-outs as secret; type="text" | A password the crew choose to hand on, such as the guest network's |
- A password belongs to an account. A manager saves a password under the username it finds in the same form,
in a field with
autocomplete="username". A form with a visible username field, such as a network's name, gives that fieldautoComplete="username"and puts it before the password. A form without one passesusername, and the field adds a hiddenautocomplete="username"input before the password, as 1Password's guidance does for password reset forms. - One form per account. Related fields share a
<form>and unrelated ones do not, so the manager does not save a Wi-Fi password and an API key as one login. - A secret is pasted trimmed. An API key or a token never starts or ends with a space, so a
secretpasted with the spaces copied around it goes in without them; hidden behind the dots, they would be saved and fail. A password of any other purpose keeps what is typed and pasted, spaces included. - A new password states its rules.
passwordRulesis written to Apple'spasswordrulesattribute, which Safari's generator and 1Password follow, together withminLengthandmaxLength. It names lengths and the characters allowed, neverrequired:classes. Chrome's generator does not reliably follow it. - A secret is not a login on the vessel. A browser saves logins per address, so a key saved on the vessel's
address would be offered in every password field there, under no username. The key belongs in the manager as its
own item (1Password has an API Credential item), from which the crew paste it.
autocomplete="off"alone does not keep it out: browsers ignore it on password fields when they offer to save, so the field also writes the opt-outs the managers' extensions honor. - A paste is never cut without a word. A browser cuts a paste at
maxLengthand says nothing, so a generated password that is too long would go in short and the crew would save one their manager does not hold. The field keepsmaxLength, which the generators read, and when a paste runs past it the line under the field says how many characters were pasted, how many went in, and to set the manager's generator to that length or fewer. - Nothing is blocked. The field never stops a paste, sets
spellcheck,autocapitalizeandautocorrectoff, and keeps its id and name for as long as it is on the page. Acurrentornewpassword is named after its token,current-passwordornew-password, unless the caller names it, so a manager matches the field by its name as well as by itsautocomplete.
Sources: 1Password, Design your website to work best with 1Password; HTML Standard, Autofill; Apple, Customizing Password AutoFill rules; Chromium, Password form styles that Chromium understands; MDN, Managing autofill for login fields; web.dev, Sign-in form best practices; Stefan Judis, Turn off password managers.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| purpose | PasswordPurpose | undefined | "current" | What the password is for; it sets the autocomplete token password managers read. |
| name | string | undefined | – | The field's name, which password managers match a field by. A current or new password is named after its
token (current-password, new-password), as web.dev's forms are, unless the caller names it; a secret or a
shared password takes only the caller's. |
| value | string | undefined | – | The password, when the caller holds it. |
| defaultValue | string | undefined | – | The password it starts with when the caller does not hold value. |
| onValueChange | ((value: string) => void) | undefined | – | Called with the text as the crew type or paste it. |
| visible | boolean | undefined | – | Whether the password shows as plain text, when the caller holds it; a shared password always does. |
| defaultVisible | boolean | undefined | false | Whether the password starts shown when the caller does not hold visible; a shared password always is. |
| onVisibleChange | ((visible: boolean) => void) | undefined | – | Called with the new state when the button, a form submit or hideOnBlur shows or hides the password. |
| hideOnBlur | boolean | undefined | false | Hide the password again when focus leaves the field and its button. Submitting the form always hides it. |
| passwordRules | string | undefined | – | Apple's passwordrules for a new password, which Safari's and 1Password's generators follow: lengths and the
characters allowed, never required: character classes. A WPA2 network's is
"minlength: 8; maxlength: 63; allowed: ascii-printable;". Give minLength and maxLength as well. |
| username | string | undefined | – | The account the password belongs to, when the form shows no username field: a hidden autocomplete="username"
input before the password, so a password manager saves the password under it, such as a Wi-Fi network's name. A
form with a visible username field gives that field autoComplete="username" instead. |
| contextWords | readonly string[] | undefined | – | Names a new password must not contain, such as the vessel's and the network's. With purpose="new" or "shared",
the checklist lists the length from minLength, then these names, then requirements. |
| requirements | readonly PasswordRequirement[] | undefined | – | More rules a new password must meet, listed after the length and the names, each marked met or not with a shape and a word. NIST SP 800-63B-4 forbids composition rules: no "a capital, a small letter and a digit". |
| appearance | "plain" | "outlined" | undefined | "plain" | plain draws the kit's text input; outlined draws the maritime inputs' outlined field. |
| label | string | undefined | – | The field's name on the outline, with appearance="outlined"; omit it inside a FormField. |
| hint | ReactNode | – | Text under the outlined field, translated by the caller; inside a FormField, use its hint. |
| error | string | undefined | – | What is wrong with the value and how to fix it; announced when it appears. |
| className | string | undefined | – | Classes added to the field's root. |
| testId | string | undefined | – | data-testid on the field's root. |
| ref | Ref<HTMLInputElement> | undefined | – | The password input element. |
| motion | MotionProp | undefined | – | How the outlined field's error line moves; see OutlinedField. |
Also takes every attribute of Omit<
InputHTMLAttributes<HTMLInputElement>,
"type" | "value" | "defaultValue" | "autoComplete" | "children" | "size"
>.
Related
- Form field: the label, hint and error a plain password input sits in.
- Outlined field: the box the outlined appearance draws in.
- Input: the text input the plain appearance is drawn with.
- One-time code input: the visible field for a code sent for one use.
- Email input: the other field a browser fills from what it has saved.