DEV Community

Vin Lookup
Vin Lookup

Posted on

Sanitizing Clipboard Paste Events for VIN Inputs in the Browser

Most VIN typos are not typed. They are pasted from emails, PDFs, dealer portals, and SMS threads that wrap a 17-character code in labels, spaces, or a whole vehicle blurb. If your input only listens to change and trims the final string, users already saw a red error flash while the paste was still settling.

The browser gives you a better hook: the paste event and clipboardData. Handle that event well and you can normalize before React state, before check-digit validation, and before any NHTSA call.

This post is a practical guide to VIN-safe paste handling: read the clipboard text, strip junk that is not part of a VIN, block multi-value dumps, and keep accessible keyboard paste working. It complements ordinary trim-and-uppercase helpers by running at the moment the clipboard hits the field.

What paste actually delivers

A ClipboardEvent can expose HTML, plain text, and files. For a VIN field you almost always want plain text only.

function plainTextFromPaste(event: ClipboardEvent): string {
  const data = event.clipboardData;
  if (!data) return "";
  return data.getData("text/plain") || data.getData("text") || "";
}
Enter fullscreen mode Exit fullscreen mode

Ignore text/html. Dealer emails love <span>1HG...</span> wrappers; HTML paste paths reintroduce tags and entities you then have to undo. Also ignore file pastes from screenshots: OCR belongs in a separate flow, not in a plain VIN box.

Sanitize toward a VIN charset

ISO-style VINs use A-Z and 0-9 except I, O, and Q. Paste sanitizers should be aggressive about structure and conservative about guessing:

  • Uppercase
  • Drop whitespace and common separators (-, :, .)
  • Drop obvious labels (VIN, Veh, #)
  • Reject or truncate past 17 useful characters rather than silently picking the wrong 17 from a paragraph
const VIN_CHAR = /[A-HJ-NPR-Z0-9]/i;

export function sanitizeVinPaste(raw: string): {
  candidate: string;
  rejectedExtra: boolean;
  hadLabelNoise: boolean;
} {
  const upper = raw.toUpperCase();
  const hadLabelNoise = /\bVIN\b|\bVEHICLE\b/.test(upper);

  let cleaned = upper
    .replace(/\bVIN\b\s*[#:.-]?\s*/g, "")
    .replace(/[^A-Z0-9]/g, "");

  // Drop illegal VIN letters entirely (I, O, Q) rather than mapping them.
  cleaned = cleaned
    .split("")
    .filter((ch) => VIN_CHAR.test(ch))
    .join("");

  const rejectedExtra = cleaned.length > 17;
  const candidate = cleaned.slice(0, 17);
  return { candidate, rejectedExtra, hadLabelNoise };
}
Enter fullscreen mode Exit fullscreen mode

Do not auto-map O to 0 or I to 1 inside a paste handler. That belongs in an explicit "Did you mean?" UX if you offer it at all. Silent transliteration creates confident wrong decodes, which is worse than a clear validation error.

Wire the paste handler

Prevent the default insert, sanitize, then write the candidate into the input. That avoids a frame where the field shows a 200-character dump and your validator screams.

export function onVinPaste(
  event: ClipboardEvent,
  apply: (vin: string, meta: { rejectedExtra: boolean }) => void,
): void {
  const raw = plainTextFromPaste(event);
  if (!raw) return;

  event.preventDefault();
  const { candidate, rejectedExtra } = sanitizeVinPaste(raw);
  apply(candidate, { rejectedExtra });
}
Enter fullscreen mode Exit fullscreen mode

In React:

<input
  name="vin"
  autoComplete="off"
  spellCheck={false}
  inputMode="text"
  onPaste={(e) => {
    onVinPaste(e.nativeEvent, (vin, meta) => {
      setVin(vin);
      setHint(
        meta.rejectedExtra
          ? "Paste looked longer than a VIN; used the first 17 characters."
          : null,
      );
    });
  }}
/>
Enter fullscreen mode Exit fullscreen mode

spellCheck={false} matters: browsers love to "help" with red underlines on VIN-looking tokens. autoComplete="off" reduces the chance the browser suggests an old address fragment into the box.

Multi-line and multi-VIN pastes

Clipboard payloads from spreadsheets often include tabs and newlines. Decide product policy up front:

Paste shape Recommended UX
One token, length 17 after sanitize Accept silently
Longer blob, first 17 clear Accept + short hint
Several 17-char tokens Reject with "Paste one VIN"
Empty after sanitize Keep field unchanged + error
export function detectMultiVin(raw: string): boolean {
  const tokens = raw
    .toUpperCase()
    .split(/[\s,;]+/)
    .map((t) => t.replace(/[^A-Z0-9]/g, ""))
    .filter((t) => t.length >= 11);
  const vinLike = tokens.filter((t) => t.length === 17);
  return vinLike.length > 1;
}
Enter fullscreen mode Exit fullscreen mode

If detectMultiVin is true, skip sanitizeVinPaste's slice behavior and ask the user to paste a single code. Silently taking the first VIN from a list is how the wrong car gets decoded on a marketplace form.

Accessibility and security notes

  • Do not call navigator.clipboard.readText() on paste unless you must; the event's clipboardData is enough and avoids extra permission prompts
  • Keep Ctrl/Cmd+V working; never disable paste site-wide
  • Announce sanitizer hints with aria-live="polite" so screen reader users know why the value changed
  • Never log raw clipboard contents; log lengths and boolean flags only
  • Run the same sanitizer on blur for users who typed instead of pasted, so both paths converge before decode

Takeaway

VIN inputs live or die on paste quality. Intercept the paste event, read plain text, strip labels and illegal characters, refuse multi-VIN dumps, and tell the user when you truncated. Do that before check digits and before NHTSA, and most "invalid VIN" rage-clicks disappear without teaching anyone ISO 3779.

I maintain VIN Lookup, a free VIN decode based on NHTSA data.

Top comments (0)