Skip to content
Download ZIP

Blue accent palette · 10.2 KB ZIP File receipt View as Markdown View code

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.
cmp_password_field_01 · version 1.0.0 · Blue accent palette
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
PasswordField.svelte Svelte · 18.0 KB Raw
<!--
	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>

Give 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
Required props
labelpurpose

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. reserveError holds one line, or the number of lines you pass, so an error that fits moves nothing; a longer one still grows the field.

Example

Svelte
<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.

Svelte
{#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:

Svelte
<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:

Svelte
<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:

CSS
.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
NameTypeRequiredDefaultDescription
labelstringYesNoneVisible label, always shown. The placeholder never stands in for it.
purpose'current' | 'new'YesNone'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.
valuestringNoNoneBindable value. Undefined renders an empty field.
namestringNoNoneForm field name for native submission, usually 'password' or 'new-password'.
hintstringNoNoneHelp text under the label, such as the requirements for a new password. Linked first in aria-describedby.
errorstring | nullNonullValidation 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.
errorPrefixstringNo'Error:'Visually hidden text read before the error.
describedBystringNoNoneExtra ids for aria-describedby, after the hint and error, such as password strength feedback (C286).
allowRevealbooleanNotrueShow 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.
revealedbooleanNofalseBindable: whether the password is shown. Ignored before hydration and when allowReveal is false; set back to false on submit and reset.
warnCapsLockbooleanNotrueShow a Caps Lock chip inside the field, and announce it, while typing with Caps Lock on.
capsLockLabelstringNo'Caps Lock'Visible text of the Caps Lock chip.
capsLockMessagestringNo'Caps Lock is on'Announced politely when Caps Lock is detected.
requiredbooleanNofalseNative required attribute. No marker is drawn; sign-in fields are required by context.
reserveErrorboolean | numberNofalseHold 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).
disabledbooleanNofalseDisables 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.
labelActionSnippetNoNoneContent at the end of the label row, usually a Forgot password? link. A link inside gets quiet defaults you can override with a class.
idstringNoNoneInput id. Defaults to one generated per instance. The hint and error always take generated ids.
classstringNoNoneExtra classes on the outer wrapper, for placement.

Customization#

On this page

Change 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-accent colours 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-danger colours the error edge, the error focus ring, the icon and the message. The default is red-700 (6.5:1 on white).
  • Neutrals: --password-field-ink is the label, value and Caps Lock chip, --password-field-muted the hint and the resting button text, --password-field-placeholder the placeholder, --password-field-hairline the resting edge (keep it at 3:1, since it is the only thing marking an empty field) and --password-field-surface the fill.
  • Worked retone for a #09090b band: --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 allowReveal false only where a shared screen makes showing a password a real risk.
  • Translate revealLabels, revealNames, revealAnnouncements, capsLockLabel, capsLockMessage and errorPrefix with the page; each revealNames entry must contain its revealLabels text.
  • 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

VariableToken
--password-field-accentaccent
--password-field-dangerdanger
--password-field-inkink
--password-field-mutedmuted
--password-field-placeholderplaceholder
--password-field-hairlinehairline
--password-field-surfacesurface

Accessibility#

On this page
  • A native <label for> names the input. aria-describedby lists the hint, then the error, then describedBy and any aria-describedby you pass, each only when present; aria-invalid is 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-controls pointing at the input. Its name changes with the action (Show password, Hide password), so it deliberately has no aria-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 capsLockMessage instead.
  • 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.