Password field
A password field with the right autocomplete for sign-in or a new password, a Show/Hide button and Caps Lock notice inside one border, an optional Forgot password? slot, hint and error. Plain password input before hydration.
cmp_password_field_01 Preview
Demonstration — no data is sent
Fit to the available width. The frame follows the height of its content; previews taller than the maximum auto-height scroll inside it.
Give this component to your coding agent Copy a prompt that fetches this exact version and palette through the PageSugar MCP server.
Using the PageSugar MCP server, fetch component cmp_password_field_01 version 1.0.0 with variant "blue", first inspect its requirements and license status and confirm this project uses Svelte 5 and Tailwind CSS 4. Retrieve every manifest file, including binary assets and any manifest-only response files, preserving relative paths. Then integrate the source and follow its usage notes. Run project checks, review the browser result and report anything unverified. Do not substitute another version or invent missing files.Not connected yet? Set up the MCP server
Code
- Palette
- Blue accent
- Version
- 1.0.0
- Digest
Full digest
sha256-6daa7e87c9d84e8b935e8234aa8658b620dac6a3e79bee73205575d9eb528da0
<!--
A password field: a visible label with an optional action beside it (a "Forgot password?"
link), an optional hint, the masked input, and a consumer-supplied error. Once the page has
hydrated, a Show/Hide button and a Caps Lock notice sit inside the field's one border, so the
input and its controls read and focus as a single shape. Before that it is a plain native
password input that submits with its form. It never validates, scores or transmits anything.
-->
<script lang="ts" module>
export type PasswordFieldPurpose = 'current' | 'new';
export type PasswordFieldSize = 'sm' | 'md' | 'lg';
export type PasswordFieldRevealStyle = 'text' | 'icon';
export interface PasswordFieldRevealText {
show: string;
hide: string;
}
export interface PasswordFieldAnnouncements {
shown: string;
hidden: string;
}
</script>
<script lang="ts">
import { flushSync, onMount, type Snippet } from 'svelte';
import type { HTMLInputAttributes } from 'svelte/elements';
type Passthrough = Omit<
HTMLInputAttributes,
| 'type'
| 'value'
| 'id'
| 'name'
| 'size'
| 'required'
| 'disabled'
| 'autocomplete'
| 'spellcheck'
| 'autocapitalize'
| 'autocorrect'
| 'class'
| 'children'
| 'aria-invalid'
>;
interface Props extends Passthrough {
/** Visible label. Always shown; the placeholder never stands in for it. */
label: string;
/** Sets autocomplete: 'current' for sign-in, 'new' for sign-up and resets. */
purpose: PasswordFieldPurpose;
/** Bindable value. */
value?: string;
/** Form field name for native submission, usually 'password'. */
name?: string;
/** Help text under the label, such as the requirements for a new password. */
hint?: string;
/** Validation message from the consumer. Sets aria-invalid; the component never validates. */
error?: string | null;
/** Read before the error by screen readers only, for other languages. */
errorPrefix?: string;
/** Extra ids for aria-describedby, after the hint and error, such as strength feedback. */
describedBy?: string;
/** Show the reveal button once the page has hydrated. */
allowReveal?: boolean;
/** 'text' shows the words; 'icon' shows an eye, with the words kept for assistive technology. */
revealStyle?: PasswordFieldRevealStyle;
/** Visible button text. */
revealLabels?: PasswordFieldRevealText;
/** The button's accessible name. Each must contain its visible label. */
revealNames?: PasswordFieldRevealText;
/** Announced politely after the button is used. */
revealAnnouncements?: PasswordFieldAnnouncements;
/** Bindable: whether the password is shown. Always hidden before hydration and after submit. */
revealed?: boolean;
/** Show a Caps Lock notice while typing with Caps Lock on. */
warnCapsLock?: boolean;
/** Visible Caps Lock notice inside the field. */
capsLockLabel?: string;
/** Announced politely when Caps Lock is detected. */
capsLockMessage?: string;
/** Native required attribute. */
required?: boolean;
/** Hold space under the field for the error (true for one line, or a number of lines), so setting it moves nothing below. */
reserveError?: boolean | number;
/** Disables the input and the reveal button. */
disabled?: boolean;
/** sm 32 px, md 36 px, lg 40 px tall, matching action buttons; 44 px on touch screens. */
size?: PasswordFieldSize;
/** Content at the end of the label row, such as a "Forgot password?" link. */
labelAction?: Snippet;
/** Input id. Defaults to one generated per instance. */
id?: string;
/** Extra classes on the outer wrapper, for placement. */
class?: string;
}
let {
label,
purpose,
value = $bindable(),
name,
hint,
error = null,
errorPrefix = 'Error:',
describedBy: extraDescribedBy,
allowReveal = true,
revealStyle = 'text',
revealLabels = { show: 'Show', hide: 'Hide' },
revealNames = { show: 'Show password', hide: 'Hide password' },
revealAnnouncements = { shown: 'Password shown', hidden: 'Password hidden' },
revealed = $bindable(false),
warnCapsLock = true,
capsLockLabel = 'Caps Lock',
capsLockMessage = 'Caps Lock is on',
required = false,
reserveError = false,
disabled = false,
size = 'md',
labelAction,
id,
class: className,
...rest
}: Props = $props();
const uid = $props.id();
const inputId = $derived(id ?? `${uid}-input`);
/* Hint and error ids come from the instance, so a supplied input id can never collide with them. */
const hintId = `${uid}-hint`;
const errorId = `${uid}-error`;
const invalid = $derived(!!error);
const reservedLines = $derived(reserveError === true ? 1 : reserveError || 0);
/* Hint first, then the error, then the consumer's ids (strength feedback, a counter). */
const describedBy = $derived(
[
...new Set(
[
hint && hintId,
invalid && errorId,
...(extraDescribedBy ?? '').split(/\s+/),
...(rest['aria-describedby'] ?? '').split(/\s+/)
].filter(Boolean)
)
].join(' ') || undefined
);
let input: HTMLInputElement | undefined = $state();
/* The button and the notice need script, so the server render is a plain password input. */
let hydrated = $state(false);
let capsLock = $state(false);
let announcement = $state('');
const canReveal = $derived(hydrated && allowReveal);
const shown = $derived(canReveal && revealed);
const capsLockShown = $derived(hydrated && warnCapsLock && capsLock);
onMount(() => {
hydrated = true;
});
/*
* The type goes back to password before the form submits, so the browser offers to save the
* credential and never keeps it in its plain-text form history. The handler sets the DOM type
* itself because submission follows the submit event before Svelte would flush. It listens on
* the document and checks the input's form at event time, so a form attribute that changes,
* or a form that appears after mount, is still followed.
*/
$effect(() => {
const hide = (event: Event) => {
if (!input || !input.form || event.target !== input.form) return;
input.type = 'password';
revealed = false;
announcement = '';
};
document.addEventListener('submit', hide, true);
document.addEventListener('reset', hide, true);
return () => {
document.removeEventListener('submit', hide, true);
document.removeEventListener('reset', hide, true);
};
});
/* Switching the type resets the selection in some engines, so it is put back where it was. */
function toggle() {
if (!input) return;
const { selectionStart: start, selectionEnd: end, selectionDirection } = input;
revealed = !revealed;
flushSync();
if (start !== null && end !== null) {
input.setSelectionRange(start, end, selectionDirection ?? undefined);
}
announcement = revealed ? revealAnnouncements.shown : revealAnnouncements.hidden;
}
/*
* A press anywhere else inside the border (the edge, the padding, the Caps Lock notice) focuses
* the input, as a press inside one box should. The button handles its own presses.
*/
function focusInput(event: PointerEvent) {
if (!input || event.button !== 0 || event.target === input || input.disabled) return;
if ((event.target as Element).closest('button')) return;
event.preventDefault();
input.focus();
}
/* A press on the button keeps focus, and the caret, in the input. Keyboard focus is unaffected. */
function keepFocus(event: MouseEvent) {
if (document.activeElement === input) event.preventDefault();
}
/* The state is unknown until a key event reports it; blur forgets it, since it can change elsewhere. */
function readCapsLock(event: KeyboardEvent) {
capsLock = event.getModifierState?.('CapsLock') ?? false;
}
</script>
<div
class={['password-field @container min-w-0', className]}
data-size={size}
data-invalid={invalid ? '' : undefined}
data-reveal={canReveal ? '' : undefined}
>
<div class="flex flex-wrap items-baseline justify-between gap-x-4">
<label
for={inputId}
class={[
'min-w-0 leading-5 font-medium text-pretty break-words text-(--_ink)',
size === 'sm' ? 'text-[13px]' : 'text-sm'
]}
>
{label}
</label>
{#if labelAction}
<span class="password-field__action text-[13px] leading-5">{@render labelAction()}</span>
{/if}
</div>
{#if hint}
<p id={hintId} class="mt-1 text-[13px] leading-5 text-pretty break-words text-(--_muted)">
{hint}
</p>
{/if}
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class={[
'password-field__control mt-2 flex min-h-(--_h) items-stretch border',
'border-(--_edge) bg-(--_surface) text-(--_ink)',
size === 'sm' ? 'rounded-md' : 'rounded-lg'
]}
onpointerdown={focusInput}
>
<input
bind:this={input}
{...rest}
bind:value
type={shown ? 'text' : 'password'}
{name}
{required}
{disabled}
id={inputId}
autocomplete={purpose === 'new' ? 'new-password' : 'current-password'}
spellcheck="false"
autocapitalize="none"
autocorrect="off"
aria-describedby={describedBy}
aria-invalid={invalid ? 'true' : undefined}
class={[
'password-field__input min-w-20 flex-1 bg-transparent ps-3 text-(--_ink) outline-none',
'placeholder:text-(--_placeholder) disabled:cursor-not-allowed',
'pointer-coarse:text-base',
size === 'sm' && 'text-[13px] leading-5',
size === 'md' && 'text-sm leading-5',
size === 'lg' && 'text-base leading-6',
/* Shown passwords are set in mono, so 0 and O, l and 1 read apart. */
shown && 'font-mono',
canReveal ? 'pe-2' : 'pe-3'
]}
onkeydown={(event) => {
readCapsLock(event);
rest.onkeydown?.(event);
}}
onkeyup={(event) => {
readCapsLock(event);
rest.onkeyup?.(event);
}}
onblur={(event) => {
capsLock = false;
rest.onblur?.(event);
}}
/>
{#if capsLockShown}
<!-- The live region below announces it; this is the visible half. -->
<span
class={['flex shrink-0 items-center self-center', canReveal ? 'pe-2' : 'pe-3']}
aria-hidden="true"
>
<span
class="password-field__caps inline-flex h-5 items-center gap-1 text-[13px] leading-5 font-medium whitespace-nowrap text-(--_muted)"
>
<svg class="size-3.5 shrink-0" viewBox="0 0 16 16" fill="none">
<path
d="M8 2.75 3.25 7.5H5.5v2.75h5V7.5h2.25L8 2.75Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
<path d="M5.5 13h5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
{capsLockLabel}
</span>
</span>
{/if}
{#if canReveal}
<button
type="button"
class={[
'password-field__toggle relative m-1 grid shrink-0 place-items-center text-[13px] leading-5 font-medium',
'bg-(--_fill-rest) text-(--_muted) enabled:hover:bg-(--_fill-raised) enabled:hover:text-(--_ink) enabled:active:bg-(--_fill-pressed) disabled:cursor-not-allowed',
'transition-[color,background-color] duration-120 ease-(--_ease) enabled:active:duration-80 motion-reduce:transition-none',
'focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-(--_accent)',
size === 'sm' ? 'rounded-xs' : 'rounded-sm',
revealStyle === 'icon' ? 'w-(--_btn)' : 'px-2'
]}
aria-controls={inputId}
aria-label={shown ? revealNames.hide : revealNames.show}
{disabled}
onmousedown={keepFocus}
onclick={toggle}
>
{#if revealStyle === 'icon'}
<svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path
d="M1.75 8S4 3.75 8 3.75 14.25 8 14.25 8 12 12.25 8 12.25 1.75 8 1.75 8Z"
stroke="currentColor"
stroke-width="1.5"
stroke-linejoin="round"
/>
<circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.5" />
{#if shown}
<path
d="m2.5 13.5 11-11"
stroke="currentColor"
stroke-width="1.5"
stroke-linecap="round"
/>
{/if}
</svg>
{:else}
<!-- Both labels share one cell, so the button keeps the width of the longer one. -->
<span class={['col-start-1 row-start-1', shown && 'invisible']} aria-hidden="true"
>{revealLabels.show}</span
>
<span class={['col-start-1 row-start-1', !shown && 'invisible']} aria-hidden="true"
>{revealLabels.hide}</span
>
{/if}
</button>
{/if}
</div>
{#if invalid}
<p
id={errorId}
class="mt-1 flex min-h-(--_reserve) items-start gap-1 text-[13px] leading-5 text-(--_danger)"
style:--_reserve={reservedLines ? `${reservedLines * 1.25}rem` : undefined}
>
<span class="grid h-lh shrink-0 place-items-center" aria-hidden="true">
<svg class="size-3.5" viewBox="0 0 16 16" fill="none">
<circle cx="8" cy="8" r="6.25" stroke="currentColor" stroke-width="1.75" />
<path d="M8 4.75v3.5" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" />
<circle cx="8" cy="11" r="1" fill="currentColor" />
</svg>
</span>
<span class="min-w-0 text-pretty break-words"
><span class="sr-only">{`${errorPrefix} `}</span>{error}</span
>
</p>
{:else if reservedLines}
<div class="mt-1" style:height="{reservedLines * 1.25}rem" aria-hidden="true"></div>
{/if}
<!-- Two polite regions, present from the first render so their first message is announced. -->
<span class="sr-only" role="status">{capsLockShown ? capsLockMessage : ''}</span>
<span class="sr-only" role="status" aria-atomic="true">{announcement}</span>
</div>
<style>
/* Public tokens: set --password-field-* on the field or any ancestor to retone it. */
.password-field {
--_accent: var(--password-field-accent, #1d4ed8);
--_danger: var(--password-field-danger, #b91c1c);
--_ink: var(--password-field-ink, #18181b);
--_muted: var(--password-field-muted, #52525b);
--_placeholder: var(--password-field-placeholder, #71717a);
/* 3:1 against white: the edge is the only thing marking an empty field (WCAG 1.4.11). */
--_hairline: var(--password-field-hairline, rgb(0 0 0 / 0.44));
--_surface: var(--password-field-surface, #ffffff);
--_h: 2.25rem;
--_edge: var(--_hairline);
--_ring: var(--_accent);
--_ease: cubic-bezier(0.2, 0, 0, 1);
/* The reveal button's fill: a quiet resting step, one more on hover, one more on press. */
--_fill-rest: color-mix(in oklab, var(--_surface), var(--_ink) 4%);
--_fill-raised: color-mix(in oklab, var(--_surface), var(--_ink) 8%);
--_fill-pressed: color-mix(in oklab, var(--_surface), var(--_ink) 12%);
/* The button's side: the field's height less its border and the button's 4 px inset. */
--_btn: calc(var(--_h) - 0.625rem);
}
.password-field[data-size='sm'] {
--_h: 2rem;
}
.password-field[data-size='lg'] {
--_h: 2.5rem;
}
@media (pointer: coarse) {
.password-field[data-size] {
--_h: 2.75rem;
}
}
/* Invalid: the edge and the focus ring both carry the error colour. */
.password-field[data-invalid] {
--_edge: var(--_danger);
--_ring: var(--_danger);
}
/*
* The edge, fill and halo change through the rules below, which follow the input inside, so the
* transition that animates them lives here with them.
*/
.password-field__control {
transition-property: border-color, box-shadow, background-color;
transition-duration: 150ms;
transition-timing-function: var(--_ease);
}
@media (prefers-reduced-motion: reduce) {
.password-field__control {
transition: none;
}
}
@media (hover: hover) {
/* Hover only where typing is possible: one step darker edge. */
.password-field__control:hover:not(:has(input:disabled)) {
--_edge: color-mix(in oklab, var(--_hairline), var(--_ink) 25%);
}
.password-field[data-invalid] .password-field__control:hover {
--_edge: var(--_danger);
}
}
/* Focus in the input: a two-pixel edge in the accent (one border plus one ring), with a halo. */
.password-field__control:has(input:focus-visible) {
border-color: var(--_ring);
box-shadow:
0 0 0 1px var(--_ring),
0 0 0 4px color-mix(in oklab, var(--_ring) 14%, transparent);
}
/* Disabled: raised fill, faded, and not interactive. */
.password-field__control:has(input:disabled) {
background-color: color-mix(in oklab, var(--_surface), var(--_ink) 4%);
opacity: 0.55;
cursor: not-allowed;
}
/*
* Autofill: the browser paints its own fill on the input alone. Keep the input transparent and
* ink-coloured, and tint the whole shape instead, so the button shares the autofilled look.
*/
.password-field__input:autofill {
-webkit-text-fill-color: var(--_ink);
transition: background-color 0s 86400s;
}
.password-field__control:has(input:autofill) {
background-color: color-mix(in oklab, var(--_surface), var(--_accent) 6%);
}
/* Edge draws its own eye in password inputs; with this button present it would be a second one. */
.password-field[data-reveal] .password-field__input::-ms-reveal {
display: none;
}
/*
* The button's hit area reaches out over its 4 px inset and the field's 1 px border, so it
* spans the field's full 44 px height on touch without the visible shape growing.
*/
.password-field__toggle::after {
content: '';
position: absolute;
inset: -0.3125rem;
}
/*
* The label action is your markup (usually a link), so its defaults sit in the components
* layer under :where(), and any class you give it wins.
*/
@layer components {
.password-field__action :global(:where(a)) {
color: var(--_ink);
font-weight: 500;
text-decoration-line: none;
text-underline-offset: 3px;
border-radius: 2px;
}
/* On touch, the link's hit area grows to 44 px without moving the label row. */
@media (pointer: coarse) {
.password-field__action :global(:where(a)) {
position: relative;
}
.password-field__action :global(:where(a))::after {
content: '';
position: absolute;
/* Biased upward: it reaches into the space above the label row, never into the field. */
inset: -1.375rem -0.5rem -0.375rem;
}
}
.password-field__action :global(:where(a:focus-visible)) {
outline: 2px solid var(--_accent);
outline-offset: 2px;
}
@media (hover: hover) {
.password-field__action :global(:where(a:hover)) {
text-decoration-line: underline;
}
}
}
/* Forced colours drop shadows and repaint borders, so focus becomes a system-colour outline. */
@media (forced-colors: active) {
.password-field__control:has(input:focus-visible) {
outline: 2px solid Highlight;
outline-offset: 2px;
}
}
</style>
Usage#
On this pageGive it a label and a purpose and it renders a labelled native password input with the matching autocomplete token. Once hydrated it adds a Show/Hide button and a Caps Lock notice inside the border. Set error when your own check fails. It does not validate, score strength, check breached passwords, hash, store or send the password: that is your form and your server.
- Suggested location
src/lib/components/password-field-01
Limitations
- The component never validates or scores a password. Pair it with your own rules, and with separate strength feedback linked through
describedBy. - The Show/Hide button and the Caps Lock notice appear only after hydration; before that, and without JavaScript, the field is a plain password input.
- Caps Lock is unknown until a key is pressed in the field, and is forgotten on blur, because the page cannot read it otherwise.
- The reveal resets to hidden on the form's submit and reset events. A form submitted without a submit event (
form.submit() in script) keeps whatever type the field has. - Password manager extensions draw their own icon near the end of the field; most place it before the button, but a few may overlap it.
- The error appears below the input.
reserveErrorholds one line, or the number of lines you pass, so an error that fits moves nothing; a longer one still grows the field.
Example
<script lang="ts">
import PasswordField from '$lib/components/password-field-01/PasswordField.svelte';
let password = $state('');
let error = $state<string | null>(null);
function check(event: SubmitEvent) {
error = password ? null : 'Enter your password';
if (error) event.preventDefault();
}
</script>
{#snippet forgot()}<a href="/reset-password">Forgot password?</a>{/snippet}
<form method="POST" class="max-w-sm" onsubmit={check}>
<PasswordField
label="Password"
purpose="current"
name="password"
required
reserveError
labelAction={forgot}
bind:value={password}
{error}
/>
</form>The source imports its own files by relative path only. Examples import from $lib/...; SvelteKit 3 projects have no $lib alias by default, so import from your #lib subpath (with the .svelte extension) or a relative path instead.
Password field#
The password input for sign-in, sign-up and reset forms. It shares the label, hint and error
structure of text-field-01 and adds what a password needs: the right autocomplete token, a
Show/Hide button and a Caps Lock notice.
{#snippet forgot()}<a href="/reset-password">Forgot password?</a>{/snippet}
<PasswordField
label="Password"
purpose="current"
name="password"
required
labelAction={forgot}
bind:value={password}
{error}
/>Purpose#
purpose is required because the wrong token breaks password managers. 'current' sets
autocomplete="current-password" so a saved password fills; 'new' sets new-password so the
manager offers to generate one and saves it on submit. A change-password form uses one of each:
<PasswordField label="Current password" purpose="current" name="current-password" />
<PasswordField
label="New password"
purpose="new"
name="new-password"
hint="At least 12 characters."
/>Keep a username or email field in the same form (with autocomplete="username") so the manager
knows whose password it is.
Show and Hide#
The button appears once the page has hydrated. Without JavaScript the field is a plain password
input with no dead button. Pressing it switches the input between password and text and
keeps the caret where it was; a pointer press leaves focus in the input. On the form's submit
and reset events the field goes back to hidden, so the browser offers to save the password and
never keeps it in its plain-text form history.
The button is an ordinary action button whose name says what it will do (Show password, Hide
password), followed by a polite "Password shown" or "Password hidden". It has no aria-pressed:
a toggle state on top of a changing name is read as "Hide password, pressed".
Bind revealed to show two fields together, for example a new password and its confirmation:
<PasswordField label="New password" purpose="new" bind:revealed />
<PasswordField label="Confirm new password" purpose="new" bind:revealed />Caps Lock#
While someone types with Caps Lock on, a small chip appears inside the field beside the button and a polite live region says "Caps Lock is on". The page can only learn the state from a key event in the field, so the chip appears on the first keypress and goes on blur. It takes space inside the field, never below it, so the form does not move.
Errors#
Set error when your own check or your server rejects the password; the field shows it under
the input with an icon and sets aria-invalid. On a sign-in form, pass reserveError so the
field holds one line for it from the start and the submit button does not jump when a sign-in
fails.
Strength feedback#
The field never validates or scores. Render your requirements or strength feedback beside it and
pass its id through describedBy so it is read after the hint and error.
Retoning#
On a #09090b band:
.band {
background: #09090b;
color-scheme: dark;
--password-field-accent: #fafafa;
--password-field-ink: #fafafa;
--password-field-muted: #a1a1aa;
--password-field-placeholder: #a1a1aa;
--password-field-hairline: rgb(255 255 255 / 0.35);
--password-field-surface: #18181b;
--password-field-danger: #f87171;
}Props and content inputs#
On this page| Name | Type | Required | Default | Description |
|---|---|---|---|---|
label | string | Yes | None | Visible label, always shown. The placeholder never stands in for it. |
purpose | 'current' | 'new' | Yes | None | 'current' for sign-in (autocomplete current-password); 'new' for sign-up, reset and change forms (autocomplete new-password), so password managers fill or suggest correctly. |
value | string | No | None | Bindable value. Undefined renders an empty field. |
name | string | No | None | Form field name for native submission, usually 'password' or 'new-password'. |
hint | string | No | None | Help text under the label, such as the requirements for a new password. Linked first in aria-describedby. |
error | string | null | No | null | Validation message from your code. When set, the field shows it with an icon, turns its edge and focus ring to the danger colour and sets aria-invalid. |
errorPrefix | string | No | 'Error:' | Visually hidden text read before the error. |
describedBy | string | No | None | Extra ids for aria-describedby, after the hint and error, such as password strength feedback (C286). |
allowReveal | boolean | No | true | Show the Show/Hide button once the page has hydrated. |
revealStyle | 'text' | 'icon' | No | 'text' | 'text' shows the words; 'icon' shows an eye (struck through while shown). Either way the button's accessible name is the words. |
revealLabels | { show: string; hide: string } | No | { show: 'Show', hide: 'Hide' } | Visible button text. The button holds the width of the longer one. |
revealNames | { show: string; hide: string } | No | { show: 'Show password', hide: 'Hide password' } | The button's accessible name in each state. Each must contain its visible label (WCAG 2.5.3). |
revealAnnouncements | { shown: string; hidden: string } | No | { shown: 'Password shown', hidden: 'Password hidden' } | Announced in a polite live region after the button is used. The password itself is never announced. |
revealed | boolean | No | false | Bindable: whether the password is shown. Ignored before hydration and when allowReveal is false; set back to false on submit and reset. |
warnCapsLock | boolean | No | true | Show a Caps Lock chip inside the field, and announce it, while typing with Caps Lock on. |
capsLockLabel | string | No | 'Caps Lock' | Visible text of the Caps Lock chip. |
capsLockMessage | string | No | 'Caps Lock is on' | Announced politely when Caps Lock is detected. |
required | boolean | No | false | Native required attribute. No marker is drawn; sign-in fields are required by context. |
reserveError | boolean | number | No | false | Hold space under the field for the error: true for one line, or a number of lines for longer messages, so setting the error moves nothing below (a sign-in form's submit button stays put). |
disabled | boolean | No | false | Disables the input and the Show/Hide button. |
size | 'sm' | 'md' | 'lg' | No | 'md' | 32, 36 or 40 px tall with 13, 14 or 16 px text, matching buttons of the same size. Every size is 44 px with 16 px text on touch screens. |
labelAction | Snippet | No | None | Content at the end of the label row, usually a Forgot password? link. A link inside gets quiet defaults you can override with a class. |
id | string | No | None | Input id. Defaults to one generated per instance. The hint and error always take generated ids. |
class | string | No | None | Extra classes on the outer wrapper, for placement. |
Customization#
On this pageChange the copy through props and retone the field through seven --password-field-* CSS variables. Hover, the button's fill and the Caps Lock chip are mixed from those, so they follow any retone.
- Accent:
--password-field-accentcolours the focus edge, its halo and the button's focus ring, and tints the autofilled fill. Keep it at 3:1 against the page. - Danger:
--password-field-dangercolours the error edge, the error focus ring, the icon and the message. The default isred-700(6.5:1 on white). - Neutrals:
--password-field-inkis the label, value and Caps Lock chip,--password-field-mutedthe hint and the resting button text,--password-field-placeholderthe placeholder,--password-field-hairlinethe resting edge (keep it at 3:1, since it is the only thing marking an empty field) and--password-field-surfacethe fill. - Worked retone for a
#09090bband:--password-field-accent:#fafafa;--password-field-ink:#fafafa;--password-field-muted:#a1a1aa;--password-field-placeholder:#a1a1aa;--password-field-hairline: rgb(255 255 255 / 0.35);--password-field-surface:#18181b;--password-field-danger:#f87171. Add color-scheme: dark on the band. - Cream page such as a bakery's
#fbf7f0: leave the hairline alone (it is alpha black, so it tints with the page) and set--password-field-surface:#fffdf9. - Purpose: 'current' on sign-in, 'new' on sign-up, reset and change-password forms. A change-password form uses one of each.
- Reveal: keep the text button where there is room; the icon suits tight forms. Turn it off with
allowRevealfalse only where a shared screen makes showing a password a real risk. - Translate
revealLabels,revealNames,revealAnnouncements,capsLockLabel,capsLockMessageanderrorPrefixwith the page; eachrevealNamesentry must contain itsrevealLabelstext. - Layout: the field fills its container. Stack fields with a 20 to 24 px gap and match size to any button in the same form.
Public CSS variables
| Variable | Token |
|---|---|
--password-field-accent | accent |
--password-field-danger | danger |
--password-field-ink | ink |
--password-field-muted | muted |
--password-field-placeholder | placeholder |
--password-field-hairline | hairline |
--password-field-surface | surface |
Accessibility#
On this page- A native <label for> names the input.
aria-describedbylists the hint, then the error, thendescribedByand anyaria-describedbyyou pass, each only when present;aria-invalidis set only while error is set. - autocomplete is current-password or new-password from purpose, and paste and autofill are never blocked, so password managers work (WCAG 3.3.8 Accessible Authentication).
- The Show/Hide control is a <button type="button"> with
aria-controlspointing at the input. Its name changes with the action (Show password, Hide password), so it deliberately has noaria-pressed: a pressed state on a button whose name already changed reads as 'Hide password, pressed'. A polite status says Password shown or Password hidden; the password is never announced. - A pointer press on the button keeps focus and the caret in the input; keyboard users Tab to it after the input and focus stays on it after Enter or Space.
- The Caps Lock chip is visible text inside the field, hidden from assistive technology because a polite live region announces
capsLockMessageinstead. - spellcheck is false and autocapitalize none, so mobile keyboards neither capitalise nor autocorrect the password.
- The error is never colour alone: it has an icon, text and a visually hidden
errorPrefix. It is not a live region; move focus or announce a summary when a submit fails. - Inputs use 16 px text and grow to 44 px on touch screens; the button's hit area spans the field's height.
Known limitations
- Screen readers announce the input as a text field while the password is shown, as with every reveal control.
Release details#
On this page- Integration
- Local interaction
- Works without client-side JavaScript
- Server-side rendering supported
- Dependencies
- No additional runtime packages beyond Svelte and Tailwind CSS
- License
MIT. Default license approval is pending; see the license status before adopting the source.
- Version history
- 1.0.0 (Published) Current release · 22 October 2026
Only the current release is available. Keep downloaded source and its receipt if you need to use it again later.