Skip to content

Early previewhamur is not on npm yet. The components are being built in the open, follow along on GitHub.

InputsReact

Password Input

A password field with a button that shows the password as text and hides it again.

Rendered by React 19.3.1

Loading React…
import { Field } from '@hamur/react/field'
import { PasswordInput } from '@hamur/react/password-input'

function EyeIcon({ off = false }: { off?: boolean }) {
  return (
    <svg
      className={`password-input-icon ${off ? 'password-input-eye-off' : 'password-input-eye'}`}
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
    >
      <path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
      <circle cx="8" cy="8" r="2" />
      {off ? <path d="m2.5 2.5 11 11" /> : null}
    </svg>
  )
}

export default function PasswordInputExample() {
  return (
    <Field.Root name="password" className="field">
      <Field.Label>Password</Field.Label>
      <PasswordInput.Root className="password-input">
        <PasswordInput.Input className="input password-input-input" />
        <PasswordInput.Toggle className="password-input-toggle">
          <PasswordInput.Indicator className="password-input-indicator">
            <EyeIcon />
            <EyeIcon off />
          </PasswordInput.Indicator>
        </PasswordInput.Toggle>
      </PasswordInput.Root>
      <Field.Description className="muted">
        The eye shows the password as text, and hides it again.
      </Field.Description>
    </Field.Root>
  )
}
Not measured yetTests passing in React
Not measured yetSize
Not measured yetMemory
Not measured yetSpeed
[N] commits

Installation

npm i @hamur/react

Not on npm yet. The package goes out under the chosen name.

Anatomy

Import the component and assemble its parts:

<PasswordInput.Root>
  <PasswordInput.Input />
  <PasswordInput.Toggle>
    <PasswordInput.Indicator />
  </PasswordInput.Toggle>
</PasswordInput.Root>

Examples

Controlled

A controlled visible on the Root, changed by the Toggle and by a checkbox beside the field; the state is shown below.

PasswordInputControlled.tsx
import * as React from 'react'
import { Checkbox } from '@hamur/react/checkbox'
import { Field } from '@hamur/react/field'
import { PasswordInput } from '@hamur/react/password-input'

function EyeIcon({ off = false }: { off?: boolean }) {
  return (
    <svg
      className={`password-input-icon ${off ? 'password-input-eye-off' : 'password-input-eye'}`}
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
    >
      <path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
      <circle cx="8" cy="8" r="2" />
      {off ? <path d="m2.5 2.5 11 11" /> : null}
    </svg>
  )
}

export default function PasswordInputControlledExample() {
  const [visible, setVisible] = React.useState(false)
  return (
    <div className="stack">
      <Field.Root name="wifi" className="field">
        <Field.Label>Wi-Fi password</Field.Label>
        <PasswordInput.Root
          className="password-input"
          visible={visible}
          onVisibleChange={setVisible}
        >
          <PasswordInput.Input
            className="input password-input-input"
            defaultValue="correct horse battery staple"
          />
          <PasswordInput.Toggle className="password-input-toggle">
            <PasswordInput.Indicator className="password-input-indicator">
              <EyeIcon />
              <EyeIcon off />
            </PasswordInput.Indicator>
          </PasswordInput.Toggle>
        </PasswordInput.Root>
      </Field.Root>
      <label className="row">
        <Checkbox.Root className="checkbox" checked={visible} onCheckedChange={setVisible}>
          <Checkbox.Indicator className="checkbox-indicator">✓</Checkbox.Indicator>
        </Checkbox.Root>
        Show the password
      </label>
      <p className="muted">The password is {visible ? 'shown' : 'hidden'}</p>
    </div>
  )
}

New Password

A sign-up Form: autoComplete="new-password", required and a minimum length, a Meter and Field.Description for the strength, Field.Error on submit; sending hides the password.

PasswordInputNewPassword.tsx
import * as React from 'react'
import { Field } from '@hamur/react/field'
import { Form } from '@hamur/react/form'
import { Meter } from '@hamur/react/meter'
import { PasswordInput } from '@hamur/react/password-input'

const strengths = ['Too short', 'Weak', 'Fair', 'Good', 'Strong']
const strengthOf = (password: string) =>
  [8, 12, 16, 20].filter((length) => password.length >= length).length

function EyeIcon({ off = false }: { off?: boolean }) {
  return (
    <svg
      className={`password-input-icon ${off ? 'password-input-eye-off' : 'password-input-eye'}`}
      viewBox="0 0 16 16"
      fill="none"
      stroke="currentColor"
      strokeWidth="1.5"
    >
      <path d="M1.5 8s2.5-4.5 6.5-4.5S14.5 8 14.5 8s-2.5 4.5-6.5 4.5S1.5 8 1.5 8Z" />
      <circle cx="8" cy="8" r="2" />
      {off ? <path d="m2.5 2.5 11 11" /> : null}
    </svg>
  )
}

export default function PasswordInputNewPasswordExample() {
  const [password, setPassword] = React.useState('')
  const [created, setCreated] = React.useState('')
  const strength = strengthOf(password)
  return (
    <Form
      className="stack"
      onFormSubmit={(values) => setCreated(`Created an account for ${String(values.email)}`)}
    >
      <Field.Root name="email" className="field">
        <Field.Label>Email</Field.Label>
        <Field.Control
          type="email"
          className="input"
          autoComplete="email"
          required
          placeholder="ada@example.com"
        />
        <Field.Error className="error" />
      </Field.Root>
      <Field.Root name="password" className="field">
        <Field.Label>Password</Field.Label>
        <PasswordInput.Root className="password-input">
          <PasswordInput.Input
            className="input password-input-input"
            autoComplete="new-password"
            required
            minLength={8}
            onValueChange={setPassword}
          />
          <PasswordInput.Toggle className="password-input-toggle">
            <PasswordInput.Indicator className="password-input-indicator">
              <EyeIcon />
              <EyeIcon off />
            </PasswordInput.Indicator>
          </PasswordInput.Toggle>
        </PasswordInput.Root>
        <Meter.Root
          className="range password-input-meter"
          value={strength}
          max={4}
          getAriaValueText={(_, value) => strengths[value]}
        >
          <Meter.Label className="muted">Strength</Meter.Label>
          <Meter.Track className="range-track">
            <Meter.Indicator className="range-fill" />
          </Meter.Track>
        </Meter.Root>
        <Field.Description className="muted">
          {strengths[strength]}. Use at least 8 characters; a longer one is stronger.
        </Field.Description>
        <Field.Error className="error" match="valueMissing">
          Choose a password.
        </Field.Error>
        <Field.Error className="error" match="tooShort">
          Use at least 8 characters.
        </Field.Error>
      </Field.Root>
      <button className="button" type="submit">
        Create account
      </button>
      {created ? <p className="muted">{created}</p> : null}
    </Form>
  )
}

API reference

12 props across 4 parts, in React syntax. className, style, render are listed once, under Shared props.

IndicatorPasswordInput.Indicator

The icon inside the toggle, always mounted and hidden from assistive technology; swap an eye and an eye-off with CSS on data-visible. Renders a <span>.

Indicator data attributes
Data attributeDescription
data-visiblePresent while the password is shown as text.

InputPasswordInput.Input

The password field, a native input that works with Field as Input does. Its type is password, or text while the password is shown. Renders an <input>.

Input props
PropTypeDefault
onValueChangeCallback fired when the value changes.((value: string, eventDetails: { reason: "none"; event: Event; cancel: () => void; allowPropagation: () => void; isCanceled: boolean; isPropagationAllowed: boolean; trigger: Element | undefined; }) => void)—
defaultValueThe default value of the input.string | number | readonly string[]—
valueThe value of the input.string | number | readonly string[]—
autoCompleteThe autocomplete token list.string'current-password'
autoCapitalizeThe autocapitalize hint for virtual keyboards.string'off'
spellCheckWhether the browser checks the spelling of the value.Booleanishfalse
Input data attributes
Data attributeDescription
data-visiblePresent while the password is shown as text.
data-disabledPresent when the input is disabled.
data-validPresent when the input is in a valid state (when wrapped in Field.Root).
data-invalidPresent when the input is in an invalid state (when wrapped in Field.Root).
data-touchedPresent when the input has been touched (when wrapped in Field.Root).
data-dirtyPresent when the input's value has changed (when wrapped in Field.Root).
data-filledPresent when the input is filled (when wrapped in Field.Root).
data-focusedPresent when the input is focused (when wrapped in Field.Root).

RootPasswordInput.Root

Groups the input and the toggle that shows or hides the password, and holds whether it is shown. Renders a <div>.

Root props
PropTypeDefault
visibleWhether the password is shown as text.boolean—
defaultVisibleWhether the password is initially shown as text.booleanfalse
onVisibleChangeCallback fired before the visibility changes: on a toggle press, and when the form is((visible: boolean, eventDetails: PasswordInputRootChangeEventDetails) => void)—
disabledWhether the input and the toggle should ignore user interaction.booleanfalse
labelsThe texts of the password input: toggle, the toggle's name when it has no aria-label orPartial<PasswordInputLabels>—
Root data attributes
Data attributeDescription
data-visiblePresent while the password is shown as text.
data-disabledPresent when the password input is disabled.

TogglePasswordInput.Toggle

A button that shows or hides the password. Its name stays "Show password" and aria-pressed states whether the password is shown. Renders a <button>.

Toggle props
PropTypeDefault
nativeButtonWhether the component renders a native <button> element when replacing itbooleantrue
Toggle data attributes
Data attributeDescription
data-visiblePresent while the password is shown as text.
data-disabledPresent when the password input is disabled.

Shared props

Every part that renders an element takes these.

Shared props
PropType
classNameCSS class applied to the element, or a function that returns a class based on the component's state.string | ((state: State) => string | undefined)
styleStyle applied to the element, or a function that returns a style object based on the component's state.React.CSSProperties | ((state: State) => React.CSSProperties | undefined)
renderAllows you to replace the component's HTML element with a different tag, or compose it with another component.React.ReactElement<unknown, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, State>

Keyboard and accessibility

Keyboard

Keyboard
KeysDoesPart
TabMoves from the field to the button, which is always a Tab stopToggle
EnterSpaceShows or hides the password and leaves focus on the buttonToggle
EnterIn the field, submits its form, which hides the password before sendingInput

Accessibility

What the parts set
PartSetsSo that
Inputtype="password" or "text", autocomplete, autocapitalize="off", spellcheck="false"; no name of its ownthe field is named by its Field.Label, password managers fill it, and the text is not sent to a spell checker
Togglea button of type button with aria-pressed, aria-controls the Input's id, and the fixed name "Show password" (labels)readers say "Show password, toggle button, pressed" or "not pressed"; the name does not flip with the state
Indicatoraria-hidden="true", always mountedthe icons are not read beside the button's name
Roota div with data-visible and data-disabled; disabled disables the Input and the Togglethe field and its button cannot disagree; a read-only Input still lets the password be shown

Focus order

  1. The field, then the button: the Toggle is always in the Tab order and nothing takes it out.
  2. A pointer press on the button keeps focus in the field, with its caret and selection, and focuses the field when it was not focused; a key press leaves focus on the button.
  3. Nothing takes focus on mount or when the password is hidden by a submit or a reset.