Designing an IBAN input field: formatting, validation, errors and paste

One text field, a lot of edge cases: spaces, lowercase, pasted statements, mobile keyboards, screen readers and right-to-left pages. A practical design for the IBAN field, with a short HTML and JavaScript example that was tested.

By the Generate Random IBAN editorial team · · 10 min read

An IBAN input field should accept whatever the user has in front of them (a statement with spaces, a lowercase string from an email, a value with the word "IBAN" in front), show it in groups of four so it can be checked by eye, and send the electronic form to the server. It should validate once the user has finished typing, not while they are halfway through, and when something is wrong it should say which of the three likely things it is: the country code, the length or the check digits. The rest of this post goes through each decision, with the reason behind it, and ends with a complete example.

Accept what people actually type and paste

The IBAN has two official presentations, both defined in ISO 13616. The electronic format has no separators: DE89370400440532013000. The print format splits it into groups of four, DE89 3704 0044 0532 0130 00, and that is how it appears on cards, statements and invoices. People copy the print form. Some statements prefix it with the word "IBAN", some emails lowercase it, some PDFs replace spaces with non-breaking spaces or line breaks, and a few users type hyphens between groups because that is what they do with card numbers.

So the first rule is: normalise, do not reject. Strip everything that is not a letter or digit, drop a leading "IBAN" label, and uppercase. A field that shows "invalid IBAN" because of a space is the most common complaint about these forms, and the fix belongs in the form.

The second rule is: never block paste. Some payment forms disable the paste event on account fields, usually copied from a card-number pattern that was meant to defeat autofill. For an IBAN it only forces people to retype 22 to 34 characters, which is where the typos come from. Handle the input event, which fires after a paste as well as after a keystroke, and normalise there.

Show groups of four, store the electronic form

Formatting as the user types is worth doing, because the groups of four are what makes an IBAN checkable against the paper it came from. The implementation has one trap: if you rewrite field.value while the caret is in the middle of the string, the caret jumps to the end. Count the alphanumeric characters to the left of the caret before you reformat, then put the caret back after that many characters in the new string. The example at the end does this in six lines.

What goes to the server is the electronic form. Either normalise on submit (the example replaces the field's value with the compact string in the submit handler) or let the server normalise, and ideally both, because the server will also receive requests that did not come from your form. Store the compact form, and format for display when you read it back. https://generaterandomiban.com/en/ibanpedia/what-is-an-iban covers the two formats in more detail.

The attributes that matter

The field is an ordinary type="text". The other attributes do more than they look.

Attribute Value Why
type text IBANs contain letters (GB29 NWBK…, NL91 ABNA…), so number is wrong, and number fields have their own problems: scroll-wheel increments and silent rejection of non-numeric input. The GOV.UK Design System tells teams not to use it unless research shows a need.
inputmode leave it at the default text inputmode="numeric" gives a digits-only keyboard on phones, which is right for a card number and wrong for an IBAN: 50 of the 91 countries in the registry allow letters in the BBAN (the UK, Ireland, the Netherlands, France and Italy among them). The attribute changes the keyboard only; it enforces nothing.
autocomplete off The HTML specification's list of autofill field names has cc-number and the other card tokens, but no token for an IBAN or bank account. There is nothing truthful to put here except off, which tells the browser not to store or offer values for the field.
autocapitalize characters Capitalises every character on virtual keyboards, so iOS and Android users see uppercase as they type. It has no effect on physical keyboards and is not supported everywhere, so you still uppercase in code.
autocorrect off Stops the keyboard "correcting" NWBK into a word. Standardised in HTML and Baseline since 2026; older browsers ignore it, which costs nothing.
spellcheck false No red underline under a bank code.
maxlength 42 34 characters is the ISO 13616 maximum, plus 8 spaces in print format. Do not set it to 34 if you format with spaces, or long IBANs get cut off.
dir ltr See the section on right-to-left pages.

Do not use a placeholder as the only hint. It disappears on the first keystroke and often fails contrast requirements. Put the example in visible hint text under the label and connect it with aria-describedby.

Mobile keyboards

With type="text", autocapitalize="characters" and autocorrect="off", a phone shows its normal letter keyboard in caps mode with suggestions off. That is the best available: there is no "letters and digits, uppercase, no suggestions" keyboard you can ask for, and the digits-only keyboards (numeric, tel) lose the letters. If most of your users are in a country whose BBAN is all digits, you could switch inputmode to numeric once the typed country code is one of those, but the gain is small and the switch itself can be disorienting mid-entry.

When to validate: on blur first, then live

Validating on every keystroke from the first character produces an error message for the whole time the user is typing a correct IBAN, which teaches them to ignore it. Validating only on submit makes them scroll back up to find out what was wrong.

The pattern that works is in the W3C's form tutorial: for data with a fixed format, validate when the user leaves the field. If that first check fails, switch to live validation for that field, so the message disappears the moment the IBAN becomes valid and the user does not have to tab out again to find out. If the field is empty on blur, say nothing; the required check belongs to submit.

The browser's built-in constraint validation can do part of this (setCustomValidity() with a message, or an empty string to clear it, then reportValidity()), but the native bubble is not styleable, disappears on its own and is not reliably announced, so most teams render their own message and add novalidate to the form.

Error messages that say which thing is wrong

An IBAN fails for one of a small number of reasons, and the field can tell them apart in order. Check in this sequence and stop at the first failure:

  1. Empty: "Enter an IBAN."
  2. Not starting with two letters: "An IBAN starts with a two-letter country code, for example DE or FR."
  3. Unknown country: "We do not recognise the country code XX." (If you only accept some countries, say that here: it is a policy, not a typo.)
  4. Positions 3 and 4 not digits: "Positions 3 and 4 of an IBAN are always digits."
  5. Wrong length for the country: "A German IBAN has 22 characters. This one has 21." Lengths come from the IBAN registry; https://generaterandomiban.com/en/ibanpedia/iban-formats-by-country lists them.
  6. Check digits wrong: "The check digits do not match the rest of the IBAN. Compare it with your statement, group by group."

The length message is the most useful of the six. The check-digit test catches a missing character too, but "check digits wrong" sends the user hunting for a wrong digit when the problem is a missing one. Telling them the expected and actual count points straight at it. For the check digits themselves, the message should not try to explain MOD 97 (https://generaterandomiban.com/en/ibanpedia/iban-check-digits does that); it should tell the user what to do, which is to compare group by group.

WCAG 2.2 asks for exactly this: success criterion 3.3.1 wants the field in error identified and the error described in text, and 3.3.3 wants a suggestion for fixing it where one is known. "Invalid IBAN" meets neither.

Accessibility: label, hint, announced errors

  • A visible <label for="iban">. "IBAN" is fine as the label text, with the hint underneath for people who do not know the term.
  • aria-describedby="iban-hint iban-error" on the input, listing both the hint and the (initially empty) error element. Screen readers then read the hint on focus and the error once it has text.
  • The error element always exists in the DOM, with aria-live="polite". When you set its text, assistive technology announces it without moving focus. Use role="alert" (assertive) only for a submit-time summary, not for a field-level message that may change as the user types.
  • aria-invalid="true" on the field while it has an error, and "false" once it is fixed.
  • Do not rely on colour alone: the text of the message is the signal; a red border is decoration.
  • Keep focus where it is. Moving focus to the error, or to the top of the form, on blur is disorienting. On a failed submit, moving focus to the first invalid field is the expected behaviour.

Right-to-left pages

An IBAN is a left-to-right string even on an Arabic or Hebrew page: the country code is on the left, and the groups read left to right. In an RTL document, an input inherits direction: rtl, so the caret starts on the right and a mixed string of letters and digits can display with its groups reordered by the bidi algorithm. Put dir="ltr" on the input itself (the HTML attribute, not CSS, because the direction is a property of the content). The label, the hint and the error message stay in the page direction. This site does the same for every IBAN it renders, including on its Arabic pages.

The complete example

The code below is what the sections above describe: normalisation, formatting with caret preservation, on-blur validation that becomes live after the first error, specific messages, and the electronic form on submit. The length table holds six countries to keep it short; a real form loads the full registry table. I tested the script's logic in Node and the event flow against a minimal DOM stub (typing, inserting mid-string, pasting an IBAN: prefixed value, blur, submit).

<form method="post" action="/payee" novalidate>
  <label for="iban">IBAN</label>
  <p id="iban-hint">As printed on the statement, for example DE89 3704 0044 0532 0130 00. Spaces are fine.</p>
  <input id="iban" name="iban" type="text" dir="ltr"
         autocomplete="off" autocapitalize="characters" autocorrect="off" spellcheck="false"
         maxlength="42" aria-describedby="iban-hint iban-error" required>
  <p id="iban-error" class="error" aria-live="polite"></p>
  <button type="submit">Save payee</button>
</form>

<script>
// Lengths from the IBAN registry; load the full table for production.
const IBAN_LENGTHS = { DE: 22, FR: 27, GB: 22, IT: 27, ES: 24, NL: 18 };
const COUNTRY_NAMES = { DE: 'German', FR: 'French', GB: 'UK', IT: 'Italian', ES: 'Spanish', NL: 'Dutch' };

function toElectronic(value) {
  return value.replace(/^\s*IBAN:?\s*/i, '').replace(/[^0-9a-z]/gi, '').toUpperCase();
}

function toPrint(electronic) {
  return electronic.replace(/(.{4})(?=.)/g, '$1 ');
}

function mod97(iban) {
  const rearranged = iban.slice(4) + iban.slice(0, 4);
  let remainder = 0;
  for (const ch of rearranged) {
    remainder = Number(String(remainder) + parseInt(ch, 36)) % 97; // 'A' -> 10 ... 'Z' -> 35
  }
  return remainder;
}

// '' when the IBAN is fine, otherwise one specific message.
function ibanError(electronic) {
  if (electronic === '') return 'Enter an IBAN.';
  const country = electronic.slice(0, 2);
  if (!/^[A-Z]{2}$/.test(country)) return 'An IBAN starts with a two-letter country code, for example DE or FR.';
  const expected = IBAN_LENGTHS[country];
  if (!expected) return `We do not recognise the country code ${country}.`;
  if (!/^[A-Z]{2}[0-9]{2}[A-Z0-9]*$/.test(electronic)) return 'Positions 3 and 4 of an IBAN are always digits.';
  if (electronic.length !== expected) {
    return `A ${COUNTRY_NAMES[country]} IBAN has ${expected} characters. This one has ${electronic.length}.`;
  }
  if (mod97(electronic) !== 1) return 'The check digits do not match the rest of the IBAN. Compare it with your statement, group by group.';
  return '';
}

const field = document.getElementById('iban');
const error = document.getElementById('iban-error');
let showErrorsWhileTyping = false;

function validate() {
  const message = ibanError(toElectronic(field.value));
  error.textContent = message;
  field.setAttribute('aria-invalid', message ? 'true' : 'false');
  return message === '';
}

field.addEventListener('input', () => {
  // Characters (not spaces) to the left of the caret, so it can be put back after formatting.
  const alnumBefore = toElectronic(field.value.slice(0, field.selectionStart)).length;
  field.value = toPrint(toElectronic(field.value));
  let pos = 0;
  for (let seen = 0; seen < alnumBefore && pos < field.value.length; pos++) {
    if (field.value[pos] !== ' ') seen++;
  }
  field.setSelectionRange(pos, pos);
  if (showErrorsWhileTyping) validate();
});

field.addEventListener('blur', () => {
  if (field.value !== '') showErrorsWhileTyping = !validate();
});

field.form.addEventListener('submit', (event) => {
  if (!validate()) {
    event.preventDefault();
    showErrorsWhileTyping = true;
    field.focus();
    return;
  }
  field.value = toElectronic(field.value); // store the electronic form
});
</script>

Two notes on the script. mod97() reduces the number one character at a time, so it never needs BigInt; https://generaterandomiban.com/en/blog/validate-iban-javascript explains that arithmetic and the alternatives. And ibanError() is the only function with user-facing text, which keeps translation in one place.

The server must repeat the same checks. The browser version exists for the user's benefit; the server's version is the one that protects your data, and it can go further (national check digits, a bank code lookup) than a form field should.

To exercise the field, you need IBANs of every shape: short ones (Norway, 15 characters), long ones (Malta, 31), ones with letters in the BBAN, and ones from countries you do not support. Generating them by hand is how test data ends up with wrong check digits.

Sources

Need test IBANs?

Generate structurally valid IBANs for 91 countries, with correct check digits, ready to copy or download as CSV, JSON or SQL. They are not issued by any bank.

Open the IBAN generator