Skip to content
ShineUI
Install

Password

Secrets with a strength check.

<sui-password>Text inputsStable

About

Password field with a show and hide eye and an optional four-step strength meter. The meter runs deep orange, orange, navy, navy: strong is never green. Form-associated: it posts name=value and binds to React, Vue (v-model) and Angular forms.

Usage

<sui-password label="Password" name="password" autocomplete="current-password"></sui-password> <script type="module"> import { defineCustomElements } from "@shineui/core/loader"; import "@shineui/core/shineui.css"; defineCustomElements(); const el = document.querySelector("sui-password"); el.addEventListener("suiChange", (e) => console.log(e.detail)); </script>
import { SuiPassword } from "@shineui/react"; import "@shineui/core/shineui.css"; export function Example() { return <SuiPassword label="Password" name="password" autocomplete="current-password" onSuiChange={(e) => console.log(e.detail)} />; }
<script setup> import { SuiPassword } from "@shineui/vue"; import "@shineui/core/shineui.css"; const onChange = (e) => console.log(e.detail); </script> <template> <SuiPassword label="Password" name="password" autocomplete="current-password" @sui-change="onChange" /> </template>
import { Component } from "@angular/core"; import { SuiPassword } from "@shineui/angular"; @Component({ standalone: true, imports: [SuiPassword], template: `<sui-password label="Password" name="password" autocomplete="current-password" (suiChange)="onChange($event)"></sui-password>`, }) export class Example { onChange(e: CustomEvent) { console.log(e.detail); } }

Basic

The eye is a toggle button: Tab reaches it, aria-pressed tracks it, and its name reads "Show password" or "Hide password".

<div style="max-width:340px"> <sui-password label="Password" name="password" autocomplete="current-password"></sui-password> </div>

With meter

show-strength shows the four-step meter, described with the field and announced politely as the step changes. Strong is navy, never green.

<div style="max-width:340px"> <sui-password label="New password" value="Falls2026!" show-strength autocomplete="new-password"></sui-password> </div>

Labels

The meter words and the eye's name are props, so one translated form can set them without registering anything. The colors stay with the steps, not the words.

<div style="max-width:340px"> <sui-password label="Mot de passe" value="Falls2026" show-strength weak-label="Faible" fair-label="Moyen" good-label="Bon" strong-label="Fort" show-label="Afficher le mot de passe" hide-label="Masquer le mot de passe"></sui-password> </div>

Locale

registerLocale takes a page's words once, and every field with that locale picks them up; Intl cannot do this: CLDR has no word for "Weak". A regional tag falls back to its language, an unregistered word stays English, and a label prop still wins. Left unset, locale follows setLocale or html lang.

<div style="display:grid;gap:14px;max-width:340px"> <sui-password id="pw-fr" label="Mot de passe" value="Falls2026" show-strength></sui-password> <sui-password id="pw-de" label="Passwort" value="abc" show-strength help="Two words registered for de, so the rest stay English."></sui-password> </div>
// setup(root): properties and events setup(root) { import('../../packages/core/dist/index.js').then((core) => { core.registerLocale('fr-FR', { 'password.weak': 'Faible', 'password.fair': 'Moyen', 'password.good': 'Bon', 'password.strong': 'Fort', 'password.show': 'Afficher le mot de passe', 'password.hide': 'Masquer le mot de passe', }); core.registerLocale('de', { 'password.weak': 'Schwach', 'password.show': 'Passwort anzeigen' }); root.querySelector('#pw-fr').locale = 'fr-FR'; root.querySelector('#pw-de').locale = 'de-AT'; }).catch(() => { }); }

Sizes

md is the default, one height with every select, picker and button. lg is the roomier field for a sign-in page; sm packs into table rows. The eye follows the height.

<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:14px;align-items:end"> <sui-password label="Large" size="lg" value="Falls2026!"></sui-password> <sui-password label="Medium" size="md" value="Falls2026!"></sui-password> <sui-password label="Small" size="sm" value="Falls2026!"></sui-password> </div>

States

The eye stays live on a read-only field: revealing a value you may not change is the point. A disabled field disables the eye too and posts nothing.

<div style="display:grid;gap:14px;max-width:340px"> <sui-password label="API secret" value="sk-live-8841" readonly help="Rotate it from the integrations page."></sui-password> <sui-password label="Password" value="Falls2026!" disabled></sui-password> <sui-password label="Password" required error="Password is required."></sui-password> </div>

In a form

The field posts one entry under its host name. Reset restores the value it mounted with, which for a password is usually empty.

Submit to see what posts.
<form id="pw-form" style="display:grid;gap:14px;max-width:340px"> <sui-password label="New password" name="password" required show-strength autocomplete="new-password"></sui-password> <div style="display:flex;gap:8px"> <sui-button type="submit" variant="brand" label="Set password"></sui-button> <sui-button type="reset" variant="outline" label="Reset"></sui-button> </div> <output id="pw-out" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Submit to see what posts.</output> </form>
// setup(root): properties and events setup(root) { const form = root.querySelector('#pw-form'); const out = root.querySelector('#pw-out'); form.addEventListener('submit', (e) => { e.preventDefault(); out.textContent = [...new FormData(form)].map(([k, v]) => `${k}=${'*'.repeat(v.length) || '(empty)'}`).join(' '); }); form.addEventListener('reset', () => { out.textContent = 'Reset. Submit to see what posts.'; }); }

Events

suiInput fires on every keystroke, suiChange on commit at blur or Enter. Both carry the value; the log shows its length only. suiFocus and suiBlur follow the input, so a press on the eye reads as a blur.

Idle.
<div style="display:grid;gap:10px;max-width:340px"> <sui-password id="pw-events" label="Password" name="password" autocomplete="off"></sui-password> <span id="pw-log" style="font:460 13px/1.6 var(--font-sans);color:var(--ink-muted)">Idle.</span> </div>
// setup(root): properties and events setup(root) { const log = root.querySelector('#pw-log'); const el = root.querySelector('#pw-events'); el.addEventListener('suiFocus', () => { log.textContent = 'suiFocus'; }); el.addEventListener('suiBlur', () => { log.textContent = 'suiBlur'; }); el.addEventListener('suiInput', (e) => { log.textContent = `suiInput ${e.detail.length} characters`; }); el.addEventListener('suiChange', (e) => { log.textContent = `suiChange ${e.detail.length} characters`; }); }

Keyboard

KeyAction
TabMoves to the field, then to the eye control, then on; the host itself takes no tab stop.
Printable charactersType into the native input; suiInput fires on every keystroke.
Space EnterOn the eye control, shows or hides the value.

API

<sui-password>

Properties

PropertyAttributeTypeDefaultDescription
autocompleteautocompletestringstring | undefinedNative autocomplete hint, e.g. "current-password" or "new-password". inputmode and enterkeyhint set on the host reach the input the same way, and spellcheck and autocapitalize on the host apply to it as they do to any descendant.
disableddisabledbooleanfalseDisables the field and the eye.
errorerrorstringstring | undefinedError message; paints the field in the danger tone and sets aria-invalid.
fairLabelfair-labelstringstring | undefinedMeter word for the second step. Overrides the locale; defaults to "Fair".
goodLabelgood-labelstringstring | undefinedMeter word for the third step. Overrides the locale; defaults to "Good".
helphelpstringstring | undefinedHelp text under the field. Replaced by error when set.
hideLabelhide-labelstringstring | undefinedAccessible name of the eye while the value is shown. Overrides the locale; defaults to "Hide password".
invalidinvalidbooleanfalseDanger border without a message (validation handled elsewhere).
labellabelstringstring | undefinedField label. Without one the field takes the host's aria-label or aria-labelledby as its name; a visible label wins over both.
localelocalestringstring | undefinedBCP 47 language tag naming the words the meter and the eye use. They come from registerLocale() in utils/locale, which the page calls once with its own translations; there is no CLDR data for "Weak", so this cannot be read out of Intl. Set it only where one field speaks a different language from its page: unset, the field takes the page's locale, from setLocale() or <html lang>. A tag with nothing registered, or a missing word, falls back to English, and a label prop set here still wins.
maxLengthmaxlengthnumbernumber | undefinedMaximum number of characters, as the native maxlength.
minLengthminlengthnumbernumber | undefinedNative minlength: a value shorter than this, once the user has typed, fails as tooShort.
namenamestringstring | undefinedForm control name. Reflected: form entry construction reads the content attribute, while all three framework bindings set properties, so an unreflected name posts nothing at all.
patternpatternstringstring | undefinedNative pattern: a regular expression the whole value must match. The browser checks it and the field reports patternMismatch on its validity, the way required reports valueMissing.
placeholderplaceholderstringstring | undefinedPlaceholder text.
readOnlyreadonlybooleanfalseDisplay only: the value can be revealed and copied but not edited; the field still posts it. The eye stays live, since reading a value you may not change is the point of a read-only field.
requiredrequiredbooleanfalseMarks the field required (asterisk + valueMissing validity when empty).
showLabelshow-labelstringstring | undefinedAccessible name of the eye while the value is hidden. Overrides the locale; defaults to "Show password".
showStrengthshow-strengthbooleanfalseShows the strength meter under the field: Weak, Fair, Good or Strong, announced politely as it changes. Named for what it shows, because feedback is a 'icon' | 'text' union on sui-copy-button.
sizesize"lg" | "md" | "sm"'md'Control height: sm 28 for table rows, md 34 for forms and toolbars (the default, shared with the select family and the buttons, so a form built from defaults is one height), lg 42 for a sign-in or marketing page. The eye follows the height.
strongLabelstrong-labelstringstring | undefinedMeter word for the fourth step. Overrides the locale; defaults to "Strong".
valuevaluestring''Current value. Mutable; listen to suiInput / suiChange to observe edits.
weakLabelweak-labelstringstring | undefinedMeter word for the first step. Overrides the locale; defaults to "Weak".

Events

EventDetailReactVueDescription
suiBlurvoidonSuiBlur@sui-blurFires when the input loses focus, to the eye included; no detail. Does not bubble.
suiChangestringonSuiChange@sui-changeFires on commit (blur or Enter, native change); detail is the final value.
suiFocusvoidonSuiFocus@sui-focusFires when the input takes focus; no detail. The same pair is on every text field. Does not bubble.
suiInputstringonSuiInput@sui-inputFires on every keystroke; detail is the current value.

Methods

MethodSignatureDescription
select()select() => Promise<void>Selects the field text.
setFocus()setFocus() => Promise<void>Moves keyboard focus into the field.

Theming

Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.

See also