Skip to content

InputPassword

A password field that borrows Input's whole box and adds the three things a password needs: a reveal toggle that knows a tap from a peek, a caps-lock warning inside the field, and a timer that can put the secret back.

  • Stable
  • React 19
  • Native <input type="password">
  • Caps Lock warning
  • Form-library ready
Caps Lock is on

At least 8 characters.

Code
<InputPassword
name="password"
label="Password"
helper="At least 8 characters."
/>
<div class="wayra-label" data-placement="top" data-size="md">
<label class="wayra-label-text" for="usage-pw">Password</label>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" id="usage-pw" aria-describedby="usage-pw-hint" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<p class="wayra-label-hint" id="usage-pw-hint">At least 8 characters.</p>
</div>

label, helper and error are the only props that change the rendered structure, exactly as on Input: pass none and you get the bare field, pass any one and the component wraps it in the label contract.

Input renders a bare, styled <input> — it has no suffix slot, and giving it one would change the markup contract of every field in the library. So this component does not compose Input; it reuses its contracts. The root <div> joins the same styles/families/field.css family, which hands it the border, the fill, the radius, the height, the focus border and the five size steps for the price of a few list memberships.

<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" autocomplete="current-password" … />
<!-- the caps-lock badge appears here while the key is on -->
<span class="wayra-input-password-status" role="status"></span>
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password">…</button>
</div>

That is the same shape ComboBox takes, and it is why the box is provably identical to an Input’s at every size — a story compares the two computed styles step by step.

revealMode picks one behaviour out of the three people expect from an eye icon.

Code
<InputPassword revealMode="press" aria-label="press" />
<InputPassword revealMode="hold" aria-label="hold" />
<InputPassword aria-label="both" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="press" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="hold" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="both" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
revealMode Pointer down Release Space / Enter
press nothing click toggles and the state persists toggles
hold reveals always hides reveals while held, hides on key up or when focus leaves the toggle
both (default) reveals immediately under holdThreshold it was a tap and toggles; over it, it was a peek and the starting state returns toggles

holdThreshold is the millisecond line between a tap and a peek, and it is read in both alone. holdThreshold={0} turns every press into a peek, which is a quick way to get hold semantics with press keyboard behaviour.

The pointer half uses setPointerCapture, so a press that drifts off the button still ends on that button. mousedown is prevented, so the caret never leaves the field, and the gesture puts focus back with the caret at the end of the value.

The static previews on this page can only click. These three are the component itself — tap an eye, then hold one down and watch the difference:

A click toggles, and it stays where you left it.

Revealed only while the eye is held down.

A tap toggles; hold past 250 ms and it is a peek. Auto-hides 5 s after a tap.

Code
import { InputPassword } from "wayra-ui";
// One value across the three, so the only difference you can see is the
// gesture. Nothing here is a real password.
const VALUE = "cordillera-2026";
export function GesturesDemo() {
return (
<div className="hu-demo-col">
<InputPassword
revealMode="press"
label="press"
helper="A click toggles, and it stays where you left it."
defaultValue={VALUE}
/>
<InputPassword
revealMode="hold"
label="hold"
helper="Revealed only while the eye is held down."
defaultValue={VALUE}
/>
{/* The default mode, plus the timer from Security defaults: a peek is
never cut in half, because the countdown does not run while the
button is held. */}
<InputPassword
label="both"
helper="A tap toggles; hold past 250 ms and it is a peek. Auto-hides 5 s after a tap."
defaultValue={VALUE}
revealTimeout={5000}
/>
</div>
);
}

Three behaviours are on before you configure anything, and all three exist because a revealed password is a password on somebody’s screen.

Code
<InputPassword revealTimeout={5000} aria-label="Auto-hides after 5 s" />
<InputPassword aria-label="Hides when focus leaves" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="Auto-hides after 5 s" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="Hides when focus leaves" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
  • Leaving the field hides. Focus that leaves both the input and its toggle puts the secret back, whether a tap or a key revealed it. Moving between the input and the toggle is not leaving. A pointer press still in flight owns the visibility until it ends.
  • revealTimeout re-hides on a clock. 0, the default, turns it off. The countdown does not run while the button is held, so a long peek is never cut in half.
  • Visibility is uncontrolled, on purpose. There is no visible prop and no onVisibleChange. A secret an outside store can reveal is a secret something else can leave revealed, and defaultVisible (legacy parity) is as far as this goes.

Half of all “wrong password” errors are a capital letter, not a wrong password. The warning appears inside the field while the key is on and the field has focus, and it is announced as well as drawn: an aria-hidden icon plus a clipped role="status" message.

The message region is mounted empty from the start and only its text changes. A status is announced when its content changes, not when it is inserted already holding that content, so a region that arrived together with the warning would usually stay silent. The badge comes and goes; the region stays. Hand-written markup that drives the warning from its own script keeps the empty <span class="wayra-input-password-status" role="status"> in place the same way.

Caps Lock is on
Code
<InputPassword label="Password" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="Caps Lock is on" />
<span class="wayra-input-password-caps"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l7 7h-4v5H9v-5H5l7-7zM9 17h6v4H9z"></path></svg></span><span class="wayra-input-password-status" role="status">Caps Lock is on</span>
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>

The state is read from event.getModifierState("CapsLock") on keydown and keyup. Keyup matters more than it looks: releasing the CapsLock key itself is a keyup, so a keydown-only reading would leave the warning on screen after the user switched it off. Blur clears it. capsLockWarning={false} turns the whole thing off.

autoComplete defaults to current-password, which is what a sign-in form wants. Change it on the forms where it is wrong:

Screen autoComplete What it buys
Sign in current-password (default) the manager offers the stored password
Sign up, change password new-password the manager offers to generate one
A second “confirm” field new-password the same generated value, not the stored one
<InputPassword name="password" label="New password" autoComplete="new-password" />
<InputPassword name="confirm" label="Confirm password" autoComplete="new-password" />

Leaving current-password on a sign-up form is the common mistake: the manager fills the old password into the new-password field and never offers to save the new one.

The same four looks, eight roles and five steps Input ships, with the same meanings — a password field belongs to a form and has to match the fields around it.

Code
<InputPassword aria-label="outline" />
<InputPassword variant="filled" aria-label="filled" />
<InputPassword variant="soft" aria-label="soft" />
<InputPassword variant="ghost" aria-label="ghost" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="outline" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="filled" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="filled" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="soft" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="soft" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="ghost" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="cordillera-2026" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="ghost" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
Code
<InputPassword color="primary" aria-label="primary" />
<InputPassword color="secondary" aria-label="secondary" />
<InputPassword color="accent" aria-label="accent" />
<InputPassword color="neutral" aria-label="neutral" />
<InputPassword color="info" aria-label="info" />
<InputPassword color="success" aria-label="success" />
<InputPassword color="warning" aria-label="warning" />
<InputPassword color="error" aria-label="error" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="primary" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="primary" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="secondary" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="secondary" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="secondary" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="accent" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="accent" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="accent" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="neutral" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="neutral" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="neutral" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="info" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="info" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="info" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="success" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="success" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="success" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="warning" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="warning" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="warning" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="error" data-size="md" data-shape="rounded" data-visible="true" data-holding="false">
<input class="wayra-input-password-input" type="text" value="error" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="error" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="true" aria-label="Hide password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 4l16 16"></path><path d="M9.9 5.9A9.6 9.6 0 0 1 12 5.5c6 0 9.5 6.5 9.5 6.5a17 17 0 0 1-3.3 4"></path><path d="M6.6 7.8A16.6 16.6 0 0 0 2.5 12S6 18.5 12 18.5c1.4 0 2.7-.3 3.8-.8"></path><path d="M9.9 9.9a3.2 3.2 0 0 0 4.3 4.3"></path></svg></button>
</div>

The role is the edge at full strength, and it reaches one more thing here: the eye takes the accent while the value is revealed, which is the only in-field signal that the secret is on screen. On filled the eye switches to -content instead — an accent icon on an accent fill is invisible, the same call PinCode makes for its caret.

Code
<InputPassword size="xs" aria-label="xs" />
<InputPassword size="sm" aria-label="sm" />
<InputPassword size="md" aria-label="md" />
<InputPassword size="lg" aria-label="lg" />
<InputPassword size="xl" aria-label="xl" />
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="xs" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="xs" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="xs" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="sm" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="sm" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="sm" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="md" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="md" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="md" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="lg" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="lg" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="lg" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>
<div class="wayra-input-password" data-variant="outline" data-color="primary" data-size="xl" data-shape="rounded" data-visible="false" data-holding="false">
<input class="wayra-input-password-input" type="password" value="xl" autocomplete="current-password" autocapitalize="off" autocorrect="off" spellcheck="false" aria-label="xl" />
<button class="wayra-input-password-toggle" type="button" aria-pressed="false" aria-label="Show password"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12z"></path><circle cx="12" cy="12" r="3.2"></circle></svg></button>
</div>

The toggle is sized like the trigger DatePicker and ComboBox carry, max(24px, field height × 0.6), so a password field and a picker in one form show the same button at every step. The max() is not decoration: WCAG 2.5.8 asks for 24 × 24 CSS px of pointer target, and an xs field is about 21px tall at the default unit, so at that step the button keeps the floor and overhangs the field slightly rather than shrinking with it. One story measures the floor at all five steps, because axe’s target-size rule is off in the version this repo pins; another measures the match.

name, ref, defaultValue and onChange all come from ComponentProps<"input">, so a native form, TanStack Form or Formik work with no adapter.

react-hook-form is built in. Inside Form an InputPassword registers itself on name alone, the same native register path Input takes:

const form = useForm({ defaultValues: { password: "" } });
<Form form={form} onSubmit={signIn}>
<InputPassword name="password" label="Password" />
<Button type="submit">Sign in</Button>
</Form>

The validation message arrives in the hint slot, the field freezes while the form submits — and so does its toggle, because a live reveal button on a frozen row would be the only thing still moving — and noRhf turns all of it off for one instance.

ref points at the inner <input>, never at the wrapper: that is the element react-hook-form focuses for shouldFocusError and the element that carries the value.

InputPasswordProps extends ComponentProps<"input"> minus the attributes the component owns: size and color mean something else here, width and height go with the field’s fixed width, and type and children are managed.

Prop Type Default Description
variant "outline" | "filled" | "soft" | "ghost" "outline" Input’s four looks, unchanged.
color WayraColor "primary" Edge, focus ring, and the revealed eye.
size "xs" | "sm" | "md" | "lg" | "xl" "md" The field ladder.
shape "rounded" | "rectangle" | "pill" "rounded" Same three Input offers.
revealMode "press" | "hold" | "both" "both" See the three gestures.
holdThreshold number 250 Milliseconds separating a tap from a peek. both only.
revealTimeout number 0 Milliseconds until a revealed value hides itself. 0 is off.
defaultVisible boolean false Whether the field starts revealed. Focus leaving the field still hides it.
capsLockWarning boolean true The in-field Caps Lock warning.
showIcon ReactElement stroked eye Icon for the “show” action.
hideIcon ReactElement stroked crossed eye Icon for the “hide” action.
labels { show?, hide?, capsLock? } English Accessible names for the toggle and the warning.
autoComplete string "current-password" See Autocomplete.
label ReactNode — Visible label. Wraps the field in the label contract.
labelPlacement "top" | "left" | "floating" "top" Same three Input offers.
helper ReactNode — Persistent hint, wired into aria-describedby.
error ReactNode — Hint slot, role="alert", aria-invalid, red edge and ring.
noRhf boolean false Opts this instance out of react-hook-form entirely.
disabled boolean false Native. Takes the toggle with it.
readOnly boolean false Native. The value can still be revealed — looking is not editing.
unstyled boolean false Drops the design-system classes, keeps the data attributes.

There is no visible / onVisibleChange pair, and no type: this field is a password field.

className and style land on the wrapper, because the wrapper is the visible box. Every other native attribute — readOnly, required, minLength, onFocus, data-* of your own — goes to the inner <input>, and so does ref.

Key Behavior
Tab Two stops: the field, then its toggle. Moving between them keeps a revealed value; leaving both hides it.
Space / Enter on the toggle press and both toggle; hold reveals while held.
Everything else in the field Entirely native.
  • The toggle is a real button with aria-pressed, an aria-label that names the action available (a hidden field offers “Show password”), and aria-controls pointing at the input once the field has an id. A bare field invents no id, so the attribute is simply absent there rather than dangling.
  • The icon names the action too, not the state: a hidden field draws the open eye. That is the rule the legacy component used, and reversing it is the single most common bug in a password toggle.
  • Auto-repeat is ignored. Holding Enter on the toggle would otherwise flash the value at the keyboard repeat rate.
  • The caps-lock warning is announced, not only drawn — see Caps Lock.
  • Edge’s own reveal button is suppressed with ::-ms-reveal. Two eyes in one field is one too many, and the browser’s answers to none of the props above.
  • Windows high contrast repaints the eye from CanvasText and redraws both focus indicators as outlines, because box-shadow is dropped in that mode.
  • Reduced motion zeroes the component’s own durations rather than removing the animations, which is the states.css pattern: set a variable, never the property.

Do

One field, one toggle. type="password" is real, so the value is never announced and the manager knows what it is.

Don't

A plain text field with the characters hidden by CSS. The secret is readable by a screen reader and by anything that reads the DOM.

Do

new-password on a sign-up form. The password manager offers to generate one and to save what the user ends up with.

Don't

current-password left in place on the same form. The manager fills the old password in and never offers to save the new one.

Do

Caps Lock is on

The warning before the submit. The user fixes a capital letter instead of reading “wrong password”.

Don't

The same typed value, rejected after a round trip. Nothing on screen says which key is to blame.