diff --git a/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte
index 83438cc3..dd4abc81 100644
--- a/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte
+++ b/apps/dashboard/src/lib/components/dialogs/totp-onboarding-dialog.svelte
@@ -147,6 +147,7 @@
bind:value={setupPassword}
type="password"
placeholder="********"
+ autocomplete="current-password"
onkeydown={(e) => e.key === 'Enter' && enableTotp()}
/>
{#if verifyError}
diff --git a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte
index 6ab1f0b7..092077ed 100644
--- a/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte
+++ b/apps/dashboard/src/lib/components/dialogs/user-settings-dialog.svelte
@@ -7,6 +7,7 @@
import * as Avatar from '#lib/components/ui/avatar/index.js';
import { Button } from '#lib/components/ui/button/index.js';
import * as Dialog from '#lib/components/ui/dialog/index.js';
+ import { autofillAttributes } from '#lib/components/ui/input/autofill.js';
import { Input } from '#lib/components/ui/input/index.js';
import { Label } from '#lib/components/ui/label/index.js';
import { Skeleton } from '#lib/components/ui/skeleton/index.js';
@@ -627,11 +628,16 @@
-
+
-
+
{#if confirmPassword && newPassword !== confirmPassword}
Passwords do not match.
{/if}
@@ -914,25 +931,42 @@
No SSH keys added.
{/if}
-
+
+
diff --git a/apps/dashboard/src/lib/components/ui/input/autofill.ts b/apps/dashboard/src/lib/components/ui/input/autofill.ts
new file mode 100644
index 00000000..524cd67e
--- /dev/null
+++ b/apps/dashboard/src/lib/components/ui/input/autofill.ts
@@ -0,0 +1,43 @@
+import type { FullAutoFill } from 'svelte/elements';
+
+/** Autocomplete tokens for fields a password manager should fill. */
+const credentialTokens = new Set([
+ 'username',
+ 'current-password',
+ 'new-password',
+ 'one-time-code',
+ 'webauthn'
+]);
+
+/**
+ * Attributes that keep password managers away from a field, unless its
+ * `autocomplete` marks it as a sign-in credential or `allow` is set. Most
+ * dashboard fields name things (servers, volumes, networks, images), and
+ * 1Password, Bitwarden, LastPass and Dashlane otherwise offer to fill a
+ * person's name or a saved login into them.
+ *
+ * `allow` is for fields a password manager should fill but that have no
+ * matching `autocomplete` token, like SSH public keys
+ * (https://www.1password.dev/ssh/public-key-autofill).
+ *
+ * - 1Password: `data-1p-ignore` (https://www.1password.dev/web/compatible-website-design)
+ * - Bitwarden: `data-bwignore`
+ * - LastPass: `data-lpignore`
+ * - Dashlane: `data-form-type="other"`
+ */
+export function autofillAttributes(
+ autocomplete?: FullAutoFill | null,
+ { allow = false }: { allow?: boolean } = {}
+) {
+ const isCredential =
+ typeof autocomplete === 'string' &&
+ autocomplete.split(/\s+/).some((token) => credentialTokens.has(token));
+ if (allow || isCredential) return { autocomplete: autocomplete ?? undefined };
+ return {
+ autocomplete: autocomplete ?? ('off' as const),
+ 'data-1p-ignore': '',
+ 'data-bwignore': '',
+ 'data-lpignore': 'true',
+ 'data-form-type': 'other'
+ };
+}
diff --git a/apps/dashboard/src/lib/components/ui/input/input.svelte b/apps/dashboard/src/lib/components/ui/input/input.svelte
index 5f23cf93..266651da 100644
--- a/apps/dashboard/src/lib/components/ui/input/input.svelte
+++ b/apps/dashboard/src/lib/components/ui/input/input.svelte
@@ -1,12 +1,15 @@
{#if type === 'file'}
@@ -31,6 +38,7 @@
type="file"
bind:files
bind:value
+ {autocomplete}
{...restProps}
/>
{:else}
@@ -43,6 +51,7 @@
)}
{type}
bind:value
+ {...autofill}
{...restProps}
/>
{/if}
diff --git a/apps/dashboard/src/lib/components/ui/textarea/textarea.svelte b/apps/dashboard/src/lib/components/ui/textarea/textarea.svelte
index fc1dca49..7159e134 100644
--- a/apps/dashboard/src/lib/components/ui/textarea/textarea.svelte
+++ b/apps/dashboard/src/lib/components/ui/textarea/textarea.svelte
@@ -1,14 +1,20 @@
diff --git a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte
index d9f33042..c3bfb41b 100644
--- a/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte
+++ b/apps/dashboard/src/routes/(app)/admin/emails/+page.svelte
@@ -2,6 +2,7 @@
import { onMount, untrack } from 'svelte';
import { Button } from '#lib/components/ui/button/index.js';
import * as Dialog from '#lib/components/ui/dialog/index.js';
+ import { autofillAttributes } from '#lib/components/ui/input/autofill.js';
import { Input } from '#lib/components/ui/input/index.js';
import { Label } from '#lib/components/ui/label/index.js';
import { confirmDestructive } from '#lib/confirm.svelte.js';
@@ -447,6 +448,7 @@
Subject
Description