# Do Not Summon Password Managers on Non-Auth Fields

**MUST** · **ID:** `forms-no-password-manager-nonauth` · **Category:** forms
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/forms-no-password-manager-nonauth

> MUST: Keep reserved names (`password`) and `type="password"` off non-auth fields — password managers run heuristics over `type`/`name`/`id` and will park a credential dropdown over your search results. Give filters boring names (`q`, `filter`, `search`) with `type="search"` and `autocomplete="off"`; give OTP fields `autocomplete="one-time-code"` and `inputmode="numeric"`. This is the narrow inverse of the autofill rules, which still apply to real credential and address fields.

Keep reserved names off search and filter inputs, and claim a specific autocomplete token for OTP

This is the narrow inverse of the autofill rules, not a contradiction of them: on a real credential or address field you want the manager to fire, with meaningful autocomplete tokens and no paste blocking. This rule is about the fields that are not auth. Password managers do not read your intent — they run heuristics over type, name, and id, so a filter box that happens to be called "password" or an OTP input masked as type="password" gets a credential dropdown parked on top of the results the user was trying to read, plus a "save this login?" prompt for a code that expires in 30 seconds. Give non-auth fields boring names (q, filter, search), set type="search", and add autocomplete="off"; give OTP fields autocomplete="one-time-code" and inputmode="numeric" so the OS offers the SMS code instead of a stored password.

## Rule snippet

```tsx
<input type="search" name="q" autocomplete="off" />
<input name="otp" autocomplete="one-time-code" inputMode="numeric" />
```

## Bad — do not do this

`forms-no-password-manager-nonauth-bad`

```tsx
import { useState } from 'react';

const ITEMS = ['Netflix', 'GitHub', 'Bank of Mars', 'Figma', 'Vercel'];
const RESERVED = /pass|pwd|login|credential/i;

// The heuristic a password manager runs on every field it finds.
function credentialSignals(input: { type: string; name: string; id: string; autocomplete?: string }) {
  const hits: string[] = [];
  if (input.type === 'password') hits.push(`type="${input.type}"`);
  if (RESERVED.test(input.name)) hits.push(`name="${input.name}"`);
  if (RESERVED.test(input.id)) hits.push(`id="${input.id}"`);
  if (!input.autocomplete) hits.push('no autocomplete attribute');
  return hits;
}

export function NoPasswordManagerNonauthBad() {
  const [query, setQuery] = useState('');
  const [focused, setFocused] = useState(false);

  // A search box for stored passwords — so it got called "password".
  const field = { type: 'text', name: 'password', id: 'password' };
  const signals = credentialSignals(field);
  const overlay = focused && (field.type === 'password' || RESERVED.test(field.name));

  const results = ITEMS.filter((i) => i.toLowerCase().includes(query.toLowerCase()));

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4">
        <label htmlFor={field.id} className="block text-xs text-muted-foreground">
          Search saved items
        </label>
        <div className="relative">
          <input
            id={field.id}
            name={field.name}
            type="text"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            onFocus={() => setFocused(true)}
            onBlur={() => setFocused(false)}
            placeholder="Filter…"
            className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
          />

          <ul className="mt-2 space-y-1 text-sm text-foreground">
            {results.map((r) => (
              <li key={r} className="rounded-md bg-muted px-2 py-1">
                {r}
              </li>
            ))}
            {results.length === 0 && (
              <li className="px-2 py-1 text-xs text-muted-foreground">No matches</li>
            )}
          </ul>

          {overlay && (
            <div className="absolute inset-x-0 top-full z-10 mt-1 rounded-md border border-border bg-card p-2 shadow-lg">
              <div className="mb-1 text-[10px] uppercase tracking-wide text-muted-foreground">
                Password manager (simulated)
              </div>
              <div className="rounded px-2 py-1 text-sm text-foreground">gleb@example.com</div>
              <div className="rounded px-2 py-1 text-sm text-foreground">admin@example.com</div>
            </div>
          )}
        </div>

        <div className="space-y-1 text-xs text-error">
          <div>Manager matched search on: {signals.join(', ')}</div>
          <div>{overlay ? 'Overlay open — it is covering your results.' : 'Focus the field.'}</div>
        </div>

        <div className="space-y-1 border-t border-border pt-3">
          <label htmlFor="otp-bad" className="block text-xs text-muted-foreground">
            One-time code
          </label>
          <input
            id="otp-bad"
            name="pwd_code"
            type="password"
            className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
          />
          <div className="text-xs text-error">
            Manager matched OTP on: {credentialSignals({ type: 'password', name: 'pwd_code', id: 'otp-bad' }).join(', ')}
          </div>
        </div>
      </div>
      <p className="text-xs text-error">
        A plain filter field named <code>password</code>, with no <code>autocomplete</code>, trips
        every credential heuristic a manager has. Focus it and the extension&apos;s dropdown lands on
        top of the very results you were trying to read — and it offers to <em>save</em> whatever you
        type as a new login. The OTP field masked as <code>type=&quot;password&quot;</code> is worse:
        the manager tries to store a code that expires in 30 seconds as a permanent credential.
      </p>
    </div>
  );
}
```

## Good — do this

`forms-no-password-manager-nonauth-good`

```tsx
import { useState } from 'react';

const ITEMS = ['Netflix', 'GitHub', 'Bank of Mars', 'Figma', 'Vercel'];
const RESERVED = /pass|pwd|login|credential/i;

// The same heuristic a password manager runs — now it finds nothing to grab.
function credentialSignals(input: { type: string; name: string; id: string; autocomplete?: string }) {
  const hits: string[] = [];
  if (input.type === 'password') hits.push(`type="${input.type}"`);
  if (RESERVED.test(input.name)) hits.push(`name="${input.name}"`);
  if (RESERVED.test(input.id)) hits.push(`id="${input.id}"`);
  if (!input.autocomplete) hits.push('no autocomplete attribute');
  return hits;
}

export function NoPasswordManagerNonauthGood() {
  const [query, setQuery] = useState('');

  const field = { type: 'search', name: 'q', id: 'item-search', autocomplete: 'off' };
  const signals = credentialSignals(field);
  const otp = { type: 'text', name: 'otp', id: 'otp-good', autocomplete: 'one-time-code' };

  const results = ITEMS.filter((i) => i.toLowerCase().includes(query.toLowerCase()));

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4">
        <label htmlFor={field.id} className="block text-xs text-muted-foreground">
          Search saved items
        </label>
        <div>
          <input
            id={field.id}
            name={field.name}
            type="search"
            autoComplete="off"
            value={query}
            onChange={(e) => setQuery(e.target.value)}
            placeholder="Filter…"
            className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
          />

          <ul className="mt-2 space-y-1 text-sm text-foreground">
            {results.map((r) => (
              <li key={r} className="rounded-md bg-muted px-2 py-1">
                {r}
              </li>
            ))}
            {results.length === 0 && (
              <li className="px-2 py-1 text-xs text-muted-foreground">No matches</li>
            )}
          </ul>
        </div>

        <div className="space-y-1 text-xs text-success">
          <div>Manager matched search on: {signals.length ? signals.join(', ') : 'nothing'}</div>
          <div>No overlay — results stay readable while you type.</div>
        </div>

        <div className="space-y-1 border-t border-border pt-3">
          <label htmlFor={otp.id} className="block text-xs text-muted-foreground">
            One-time code
          </label>
          <input
            id={otp.id}
            name={otp.name}
            type="text"
            inputMode="numeric"
            autoComplete="one-time-code"
            maxLength={6}
            className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
          />
          <div className="text-xs text-success">
            Manager matched OTP on: {credentialSignals(otp).length ? credentialSignals(otp).join(', ') : 'nothing'} —{' '}
            <code>one-time-code</code> tells the OS to offer the SMS code instead.
          </div>
        </div>
      </div>
      <p className="text-xs text-success">
        Non-auth fields say so. The filter is <code>type=&quot;search&quot;</code>,{' '}
        <code>name=&quot;q&quot;</code>, <code>autocomplete=&quot;off&quot;</code> — nothing a
        credential heuristic can latch onto, so the dropdown never covers the results. The OTP field
        claims the exact token it wants (<code>one-time-code</code>), which surfaces the code from
        SMS or the authenticator rather than a saved password.
      </p>
    </div>
  );
}
```

## References

- [Vercel Web Interface Guidelines](https://github.com/vercel-labs/web-interface-guidelines)
- [MDN: The autocomplete attribute](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/autocomplete)
