Your Password Manager Keeps Hijacking a Field That Isn't a Password
Password managers pop a 'save password?' prompt on ordinary text inputs — even with autocomplete=off. Why it happens, and the contenteditable fix that stops it.
You’re building a chat box, a command bar, an inline cell editor — some plain text-entry surface that has nothing to do with credentials. And the operating system or browser keeps popping its “save this password?” tooltip right over it, or worse, autofilling someone’s saved login into it.
So you do the obvious thing: autocomplete="off". It keeps happening. You try
autocomplete="new-password", then renaming the field, then aria tweaks,
then a hidden decoy input. Some of it helps in one browser and breaks in
another. You are now losing an afternoon to a save-password tooltip.
Why autocomplete=“off” doesn’t save you
Password managers stopped trusting autocomplete="off" years ago — on
purpose. Sites used it for a long time to block managers from working, which
was bad for users (it pushed people toward weak, memorable passwords). So
browsers and OS-level managers (macOS/iCloud Passwords, Chrome, 1Password, and
friends) now deliberately ignore autocomplete="off" on anything that
smells login-shaped, and fall back to heuristics: the field’s name/id, its
type, nearby labels, whether it sits in a <form>, what’s next to it.
A text input named something like secret, key, token, pin, or login
— or just one sitting inside a form near a button — can trip those heuristics.
You can keep fighting them, but it’s a fight you’re structurally set up to
lose, because the heuristics are intentionally adversarial to “don’t
autocomplete me.”
The fix: stop using a form field
The reliable escape is to not give the password manager an <input> to latch
onto at all. Use a contenteditable element. A <div contenteditable> is
not a form control, so managers have no hook — no tooltip, no autofill, ever,
in any browser.
<div contenteditable="true" role="textbox" aria-multiline="true"
data-placeholder="Type a message…" class="composer"></div>
It gives you a stable, fully styleable text surface. The trade is that you now
own a few behaviors a real <input> gave you for free. Here’s the real-world
checklist:
- Read/write the value with
el.innerText(ortextContent) — there is no.value. - Placeholder doesn’t exist as an attribute — fake it in CSS:
.composer:empty::before { content: attr(data-placeholder); color: #888; }. - Submit on Enter: handle
keydown—Enterwithout Shift →preventDefault()and submit;Shift+Enter→ let the newline through. - Paste arrives as HTML. Intercept it and insert plain text, or a pasted
webpage will drag its formatting into your box:
el.addEventListener('paste', (e) => { e.preventDefault(); const text = e.clipboardData.getData('text/plain'); document.execCommand('insertText', false, text); // still works everywhere }); - Caret control:
focus()works; to drop the caret at the end, use aRange+Selectionrather than hoping. - Accessibility: add
role="textbox",aria-multilinewhere relevant, and a real label — you’ve left the world where the browser does this for you.
When to actually reach for this
Only when a genuine <input>/<textarea> can’t be made to behave: chat
composers, command palettes, inline editors, search-as-you-type bars that
managers keep mistaking for logins. For real login and signup forms you want
the password manager — don’t break it there. This is the tool for the in-app
text surfaces that aren’t credentials but keep getting treated like they are.
Further reading
I’m Chris Moore — an independent developer and consultant in Henderson, NV, working under Grudged LLC. I build and rescue the operational systems small teams run on. If the browser is fighting your UI, get in touch.