# IBAN prüfen JavaScript: Mod 97, Regex und ein React-Feld

> IBAN prüfen JavaScript: Mod 97 in TypeScript fängt Tippfehler, die ein Regex durchlässt. Dazu ein React-Feld, das beim Tippen formatiert, und rumänische Banken.

- Autor: [Serban Rusu](https://wingo-ui.com/blog/authors/serban), Founder of Wingo UI
- Veröffentlicht: 9. Okt. 2026
- Kategorie: [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components)
- Lesezeit: 11 Min.
- Kanonische URL: https://wingo-ui.com/de/blog/iban-validation-javascript
- Englische Fassung: https://wingo-ui.com/blog/iban-validation-javascript

## Kurz gesagt

Um eine IBAN in JavaScript zu prüfen, entfernst du die Leerzeichen, wandelst sie in Großbuchstaben um, prüfst die Länge für ihr Land, verschiebst die ersten vier Zeichen ans Ende, machst aus jedem Buchstaben zwei Ziffern (A ist 10, Z ist 35) und prüfst, ob die ganze Zahl mod 97 gleich 1 ist. Den Rest trägst du Zeichen für Zeichen weiter, denn die Zahl hat mehr Stellen, als der Typ Number in JavaScript exakt darstellen kann. Eine gültige IBAN ist korrekt aufgebaut, beweist aber nicht, dass das Konto existiert.

Deine Auszahlungseinstellungen, dein Lieferantenformular oder deine Rechnungsvorlage fragen nach einem Bankkonto, und die Leute fügen IBANs in jeder Form ein: kleingeschrieben, in Vierergruppen, mit Bindestrichen, mit den geschützten Leerzeichen aus einem PDF, manchmal mit „IBAN:“ davor. Viele Snippets zum Thema „IBAN prüfen JavaScript“ hören bei einem Regex auf, der eine vertippte Ziffer durchlässt, oder rufen `parseInt` auf und lehnen damit gültige Konten ab. In diesem Tutorial schreiben wir den Mod-97-Check in TypeScript, bauen ihn in eine zod-Regel und ein React-Feld ein, das beim Tippen formatiert, lesen die Bank aus rumänischen IBANs aus und zeigen das Ergebnis mit einem Copy-Button an. Die UI nutzt [Masked Input](https://wingo-ui.com/components/masked-input) (Pro), [Input](https://wingo-ui.com/components/input) und [Copy Button](https://wingo-ui.com/components/copy-button) (beide kostenlos) aus Wingo UI, der Bibliothek, die wir bauen. Wir sind also nicht neutral. Der Validator ist reines TypeScript, das du überall einfügen kannst.

## Warum reicht ein Regex nicht, um eine IBAN zu prüfen?

Ein Regex prüft die Form, und ein Tippfehler behält die Form. Wer nach „IBAN Regex“ sucht, findet meist diesen Ausdruck. Du wendest ihn an, nachdem du die Leerzeichen entfernt und alles in Großbuchstaben umgewandelt hast:

```ts
const IBAN_SHAPE = /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/;
```

Zwei Buchstaben für das Land, zwei Prüfziffern, dann 11 bis 30 Buchstaben oder Ziffern, denn IBANs reichen von 15 Zeichen (Norwegen) bis maximal 34. Ein Länder-Pattern ist strenger: Das für Rumänien lautet `^RO\d{2}[A-Z]{4}[A-Z0-9]{16}$`. Beide akzeptieren `RO49AAAA1B31007593840001`, also die rumänische Beispiel-IBAN aus dem IBAN-Register mit geänderter letzter Ziffer.

Genau dafür gibt es die zwei Prüfziffern. Jeder String in der rumänischen Form besteht den Regex, aber nur etwa 1 von 97 zufälligen Strings besteht mod 97: In einem Lauf mit 200.000 zufälligen Strings war es etwa 1 %. Behalte den Regex trotzdem, als ersten von drei Checks. So kann deine Meldung „das ist keine IBAN“ sagen, bevor sie „diese IBAN hat einen Tippfehler“ sagt.

## Wie funktioniert der Mod-97-Check der IBAN?

Das Verfahren heißt kurz IBAN mod 97: Verschieb die ersten vier Zeichen ans Ende, ersetz jeden Buchstaben durch zwei Ziffern (A ist 10, B ist 11, bis Z mit 35), lies das Ergebnis als eine einzige Ganzzahl und teil sie durch 97. Bei einer gültigen IBAN bleibt der Rest 1. Der [IBAN-Artikel auf Wikipedia](https://en.wikipedia.org/wiki/International_Bank_Account_Number) beschreibt diese Schritte aus ISO 13616. Wer die IBAN vergibt, berechnet die Prüfziffern als 98 minus den Rest derselben Zahl mit `00` an ihrer Stelle. Deshalb landet eine gültige IBAN bei 1.

Hier die britische Beispiel-IBAN, von Hand durchgerechnet:

| Schritt | Wert |
|---|---|
| Leerzeichen entfernen | GB29NWBK60161331926819 |
| Erste vier Zeichen ans Ende verschieben | NWBK60161331926819GB29 |
| Buchstaben in Zahlen (N 23, W 32, B 11, K 20, G 16) | 2332112060161331926819161129 |
| Rest mod 97 | 1 |

Diese Ganzzahl hat 28 Stellen, und der JavaScript-Typ `Number` ist nur bis 2^53 exakt, und 2^53 hat 16 Stellen. `Number(digits) % 97` und `parseInt(digits, 10) % 97` liefern für diese IBAN beide 5 und lehnen damit ein gültiges Konto ab. `BigInt(digits) % 97n` funktioniert. Den Rest Zeichen für Zeichen weiterzutragen funktioniert auch und spart die Umwandlung in einen Ziffern-String.

Laut Wikipedia erkennt der Check jedes einzelne falsche Zeichen und alle oder fast alle Vertauschungen benachbarter Zeichen. Wir haben das getestet. Bei 2.000 zufälligen deutschen IBANs, die nach dem Ländercode nur aus Ziffern bestehen, fiel jede geänderte Ziffer und jede Vertauschung zweier Nachbarn durch den Check. Buchstaben schwächen den Check: Bei 2.000 zufälligen IBANs im rumänischen Stil mit Buchstaben im Kontoteil haben etwa 0,35 % der Änderungen eines einzelnen Zeichens und der Nachbarvertauschungen trotzdem bestanden, ungefähr 1 von 300, denn ein Buchstabe wird zu zwei Ziffern. Machst du im Registerbeispiel aus `1B` ein `B1`, ergeben beide `111`, und `RO49AAAAB131007593840000` besteht.

## Wie prüfe ich eine IBAN in TypeScript?

Willst du eine IBAN validieren, normalisier die Eingabe und lass dann drei Checks in dieser Reihenfolge laufen: die Form, die Länge für das Land und mod 97. Gib zurück, welcher Check gescheitert ist. Dann kann das Formular das Problem erklären, statt nur „ungültig“ zu melden:

```ts
// lib/iban.ts
// die 42 IBAN-Ländercodes in den SEPA-Verfahren (EPC, August 2026), mit ihren
// Längen aus dem IBAN Registry von SWIFT, wie ibantools 4.5.4 sie mitliefert
const LENGTHS: Record<string, number> = Object.fromEntries(
  (
    "AD24 AL28 AT20 BE16 BG22 CH21 CY28 CZ24 DE22 DK18 EE20 ES24 FI18 FR27 " +
    "GB22 GI23 GR27 HR21 HU28 IE22 IS26 IT27 LI21 LT20 LU20 LV21 MC27 MD24 " +
    "ME22 MK19 MT31 NL18 NO15 PL28 PT25 RO24 RS22 SE24 SI19 SK24 SM27 VA22"
  )
    .split(" ")
    .map((entry) => [entry.slice(0, 2), Number(entry.slice(2))]),
);

export type IbanProblem = "format" | "country" | "length" | "checksum";

export const ibanLength = (country: string) => LENGTHS[country.toUpperCase()];

// " iban: ro49 aaaa-1b31 ..." -> "RO49AAAA1B31..."; \s deckt auch die geschützten Leerzeichen aus PDFs ab
export function electronicIban(input: string): string {
  return input.trim().toUpperCase().replace(/^IBAN:?/, "").replace(/[\s.-]/g, "");
}

// der Rest der umgestellten IBAN, Zeichen für Zeichen, damit die Zahl nie über einen Float hinauswächst
function mod97(iban: string): number {
  const rearranged = iban.slice(4) + iban.slice(0, 4);
  let remainder = 0;
  for (const char of rearranged) {
    // A ist 10 ... Z ist 35: ein Buchstabe bringt zwei Ziffern, also um 100 verschieben
    const value = char >= "A" ? char.charCodeAt(0) - 55 : Number(char);
    remainder = (remainder * (value > 9 ? 100 : 10) + value) % 97;
  }
  return remainder;
}

export function checkIban(input: string): { iban: string; problem: IbanProblem | null } {
  const iban = electronicIban(input);
  if (!/^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(iban)) return { iban, problem: "format" };
  const length = ibanLength(iban.slice(0, 2));
  if (!length) return { iban, problem: "country" };
  if (iban.length !== length) return { iban, problem: "length" };
  return { iban, problem: mod97(iban) === 1 ? null : "checksum" };
}

export const isValidIban = (input: string) => checkIban(input).problem === null;

// "RO49AAAA1B31007593840000" -> "RO49 AAAA 1B31 0075 9384 0000"
export const formatIban = (input: string) => electronicIban(input).replace(/(.{4})(?=.)/g, "$1 ");

// Prüfziffern für Testdaten: ibanCheckDigits("GB", "NWBK60161331926819") -> "29"
export function ibanCheckDigits(country: string, bban: string): string {
  return String(98 - mod97(`${country}00${bban}`)).padStart(2, "0");
}
```

```ts
checkIban("gb29-nwbk-6016-1331-9268-19"); // { iban: "GB29NWBK60161331926819", problem: null }
checkIban("IBAN: RO49 AAAA 1B31 0075 9384 0001"); // problem: "checksum"
checkIban("US12345678901234567"); // problem: "country"
formatIban("ro49aaaa1b31007593840000"); // "RO49 AAAA 1B31 0075 9384 0000"
ibanCheckDigits("RO", "BTRL0123456789ABCDEF"); // "17", also besteht RO17BTRL0123456789ABCDEF
```

Vier Entscheidungen aus dieser Datei solltest du übernehmen, auch wenn du den Rest neu schreibst:

- **Zuerst in Großbuchstaben umwandeln.** `"n".charCodeAt(0) - 55` ergibt 55, `"N"` dagegen 23. Ohne diesen Schritt scheitert also jede kleingeschriebene IBAN.
- **Jede Art von Leerzeichen entfernen.** `\s` in einem JavaScript-Regex erfasst auch die geschützten und schmalen Leerzeichen, die PDFs und Banking-Apps zwischen die Gruppen setzen.
- **Länder ablehnen, in die du nicht auszahlst.** Die Tabelle enthält die 42 IBAN-Ländercodes aus der [Übersicht der SEPA-Teilnehmer des EPC vom August 2026](https://www.europeanpaymentscouncil.eu/sites/default/files/kb/file/2026-08/EPC196-26%20v1.0%20Overview%20scheme%20adherence%202026-08-07.pdf), Albanien, Moldau, Montenegro, Nordmazedonien und Serbien eingeschlossen. Alles andere bekommt `"country"`, die ehrliche Antwort für ein SEPA-Auszahlungsformular. Wenn du IBANs aus allen Ländern annimmst, nimm eine Bibliothek (siehe den letzten Abschnitt).
- **Testdaten erzeugen.** `ibanCheckDigits` baut aus jedem Land und jedem Kontoteil eine gültige IBAN. So enthalten deine Fixtures nie das Konto einer echten Person.

## Wie ist die rumänische IBAN aufgebaut?

Eine rumänische IBAN hat 24 Zeichen: `RO`, zwei Prüfziffern, vier Buchstaben für die Bank und 16 Buchstaben oder Ziffern für Filiale und Konto. Die vier Bankbuchstaben sind die ersten vier Zeichen des BIC der Bank, also ihres SWIFT-Codes. `BTRL` steht demnach für Banca Transilvania (BIC `BTRLRO22`). Die Tabelle [IBAN formats by country](https://en.wikipedia.org/wiki/International_Bank_Account_Number#IBAN_formats_by_country) auf Wikipedia notiert das Format als `RO kk bbbb cccc cccc cccc cccc`.

| Positionen | Registerbeispiel | Bedeutung |
|---|---|---|
| 1 bis 2 | RO | Land |
| 3 bis 4 | 49 | Prüfziffern |
| 5 bis 8 | AAAA | Bank, die ersten vier Zeichen ihres BIC |
| 9 bis 24 | 1B31 0075 9384 0000 | Filiale und Konto, Buchstaben erlaubt |

Daraus folgen zwei Regeln. Eine Regel „nach dem Bankcode nur Ziffern“ lehnt echte rumänische Konten ab, denn der Kontoteil darf Buchstaben enthalten: IBANs der Banca Transilvania zum Beispiel enthalten direkt nach dem Bankcode Buchstaben wie `RONCRT`. Und die Bank findest du mit einem einzigen Lookup:

```ts
// lib/iban.ts (Fortsetzung)
// die vier Buchstaben nach den Prüfziffern sind der Anfang des BIC der Bank
const RO_BANKS: Record<string, string> = {
  BTRL: "Banca Transilvania",
  RNCB: "BCR",
  BRDE: "BRD",
  INGB: "ING Bank",
  RZBR: "Raiffeisen Bank",
  BACX: "UniCredit Bank",
  CECE: "CEC Bank",
  TREZ: "Staatskasse",
};

export function romanianBank(input: string): string | null {
  const iban = electronicIban(input);
  return iban.startsWith("RO") ? (RO_BANKS[iban.slice(4, 8)] ?? null) : null;
}
```

```ts
romanianBank("RO17 BTRL 0123 4567 89AB CDEF"); // "Banca Transilvania"
romanianBank("GB29 NWBK 6016 1331 9268 19"); // null
```

Zeig den Namen als Hinweis, den die Person wiedererkennt. Er beweist nichts über das Konto, und die Tabelle ist eine Momentaufnahme von Banken, die fusionieren. Als OTP Bank Rumänien Anfang 2025 in der Banca Transilvania aufging, bekamen ihre Kunden neue BTRL-IBANs, und die [Banca Transilvania schickte Arbeitgebern die neuen IBANs für Gehälter ab März 2025](https://intreb.bancatransilvania.ro/en/cum-voi-incasa-salariul-alocatia-pensia-orice-alt-venit-virat-baza-unei-conventii-banca-transilvania-va-lua-legatura-cu-institutiile-care-vireaza-aceste-sume-si-ii-va-informa-cu-privire-la-noile-iban-uri-mod-centralizat/). Eine alte OTP-IBAN besteht mod 97 trotzdem.

## Wie baue ich ein IBAN-Feld, das beim Tippen formatiert?

Nimm ein maskiertes Feld, das den Rohwert getrennt vom angezeigten Text speichert. Das Preset `iban` von Masked Input wandelt beim Tippen in Großbuchstaben um, gruppiert in Vierergruppen, hält den Cursor direkt hinter dem getippten Zeichen und entfernt Trennzeichen aus eingefügtem Text. Sobald die IBAN vollständig ist, lässt es mod 97 laufen und blendet ein Häkchen oder ein Warnsymbol ein; der rote Ring und `aria-invalid` warten, bis du das Feld verlässt. Auf Smartphones behält es mit `autoCapitalize="characters"` die Texttastatur, denn ein Ziffernblock hat keine Buchstaben für den Ländercode.

> Live-Demo (Masked Input): Tipp oder füg gb29-nwbk-6016-1331-9268-19 ein: Das Feld wandelt in Großbuchstaben um, gruppiert in Vierergruppen und zeigt ein Häkchen. Änderst du die letzte Ziffer, wird aus dem Häkchen eine Warnung. Probier sie unter [Masked Input](https://wingo-ui.com/components/masked-input) aus und installier sie mit `npx wingo-ui@latest add masked-input`.

Das Preset bringt Längen für neun Länder mit: Rumänien, Moldau, Deutschland, Großbritannien, Frankreich, Italien, Spanien, Bulgarien und Ungarn. Für jedes andere Land gruppiert es weiterhin in Vierergruppen, wartet aber auf 34 Zeichen, bevor es prüft. Eine niederländische IBAN bekommt ihr Symbol also nie. Übergib eine Maske, die sich aus derselben Tabelle berechnet wie dein Validator. Das Preset behält dabei die Umwandlung in Großbuchstaben und seine Tastatureinstellungen:

```ts
// lib/iban-mask.ts
import { ibanLength } from "@/lib/iban";

// "AA99 **** **** ..." passend zum Land, damit das Feld weiß, wann die IBAN vollständig ist
export function ibanMask(raw: string): string {
  const length = ibanLength(raw.slice(0, 2)) ?? 34;
  let mask = "AA99";
  for (let i = 4; i < length; i++) mask += (i % 4 === 0 ? " " : "") + "*";
  return mask;
}
```

Die zod-Regel macht aus jedem gescheiterten Check eine eigene Meldung und gibt deiner Datenbank das elektronische Format ohne Leerzeichen:

```ts
// lib/iban-schema.ts
import { z } from "zod";
import { checkIban, electronicIban, type IbanProblem } from "@/lib/iban";

const MESSAGES: Record<IbanProblem, string> = {
  format: "Gib die vollständige IBAN ein, beginnend mit dem Ländercode, etwa GB29 NWBK 6016 1331 9268 19",
  country: "Wir können nur an Banken in SEPA-Ländern auszahlen",
  length: "Diese IBAN ist für ihr Land zu kurz oder zu lang",
  checksum: "Diese IBAN enthält einen Tippfehler: Die Prüfziffern passen nicht",
};

export const iban = z
  .string()
  .trim()
  .min(1, { error: "Gib deine IBAN ein", abort: true })
  .check((ctx) => {
    const { problem } = checkIban(ctx.value);
    if (problem) ctx.issues.push({ code: "custom", message: MESSAGES[problem], input: ctx.value });
  })
  .transform(electronicIban);
```

Binde sie mit `Controller` von React Hook Form an, denn das Input-Element hält den maskierten Text und `onValueChange` liefert dir den Rohwert. Mit `validate={isValidIban}` sind sich das Symbol im Feld und die zod-Regel bei jeder IBAN einig:

```tsx
"use client";

import { Controller, useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { MaskedInput } from "@/components/ui/masked-input";
import { electronicIban, isValidIban, romanianBank } from "@/lib/iban";
import { ibanMask } from "@/lib/iban-mask";
import { iban } from "@/lib/iban-schema";

const schema = z.object({
  holder: z.string().trim().min(1, { error: "Gib den Namen des Kontoinhabers ein" }),
  iban,
});

type Values = z.output<typeof schema>;

export function PayoutForm({ onSave }: { onSave: (values: Values) => Promise<void> }) {
  const form = useForm<z.input<typeof schema>, unknown, Values>({
    resolver: zodResolver(schema),
    // der erste Fehler wartet auf blur, danach prüft das Feld bei jedem Tastendruck neu
    mode: "onTouched",
    defaultValues: { holder: "", iban: "" },
  });
  const { errors, isSubmitting } = form.formState;

  return (
    <form noValidate onSubmit={form.handleSubmit(onSave)} className="flex flex-col gap-4">
      <Input label="Kontoinhaber" autoComplete="name" {...form.register("holder")} error={errors.holder?.message} />
      <Controller
        control={form.control}
        name="iban"
        render={({ field, fieldState }) => (
          <MaskedInput
            preset="iban"
            mask={ibanMask}
            validate={isValidIban}
            label="IBAN"
            placeholder="GB29 NWBK 6016 1331 9268 19"
            ref={field.ref}
            value={field.value}
            onValueChange={(raw) => field.onChange(raw)}
            onBlur={field.onBlur}
            // die Maske behält Buchstaben, also würde ein eingefügtes "IBAN:" zum Ländercode
            onPaste={(event) => {
              const text = event.clipboardData.getData("text");
              if (!/^\s*iban\b/i.test(text)) return;
              event.preventDefault();
              field.onChange(electronicIban(text));
            }}
            // eine rumänische IBAN nennt ihre Bank, sobald der Bankcode da ist
            description={romanianBank(field.value) ?? "Füg sie mit oder ohne Leerzeichen ein."}
            error={fieldState.error?.message}
          />
        )}
      />
      <Button type="submit" loading={isSubmitting}>
        Auszahlungsdaten speichern
      </Button>
    </form>
  );
}
```

Die Beschreibung zeigt „Banca Transilvania“, sobald jemand `RO17BTRL` tippt. So kann die Person schnell bestätigen, dass sie das richtige Konto kopiert hat. Der `onPaste`-Handler deckt den einen Fall beim Einfügen ab, den die Maske nicht bereinigen kann. Das Feld verwirft jedes Trennzeichen, behält aber Buchstaben. Ein aus einem Kontoauszug kopiertes „IBAN: RO17 BTRL ...“ würde also zu `IB17 BTRL ...` werden und mit der Länder-Meldung scheitern. Der Handler entfernt das Label mit `electronicIban`, bevor die Maske den Text sieht.

Wenn du die Regel nicht selbst schreiben willst: Die Pro-Lib [form-validation](https://wingo-ui.com/components/form-validation) hat `createValidators().iban()`, mit einer einzigen Meldung für jeden Fehler und ohne Länderbeschränkung.

## Wie zeige ich eine IBAN so an, dass man sie kopieren kann?

Zeig sie in Vierergruppen an und kopier sie ohne Leerzeichen. Die Gruppen helfen beim Vorlesen oder beim Abgleich mit einer Papierrechnung. In die Zwischenablage gehört das elektronische Format, denn eine IBAN mit 34 Zeichen wächst mit Leerzeichen auf 42 Zeichen, und ein Bankformular mit `maxLength={34}` schneidet beim Einfügen das Ende ab.

```tsx
import { CopyButton } from "@/components/ui/copy-button";
import { electronicIban, formatIban, romanianBank } from "@/lib/iban";

export function PayoutAccount({ iban }: { iban: string }) {
  const bank = romanianBank(iban);
  return (
    <div className="flex items-center justify-between gap-3 rounded-2xl bg-surface p-4">
      <div className="min-w-0">
        <p className="font-mono text-sm">{formatIban(iban)}</p>
        {bank && <p className="text-xs text-muted-foreground">{bank}</p>}
      </div>
      <CopyButton value={electronicIban(iban)} copyLabel="IBAN kopieren" />
    </div>
  );
}
```

Copy Button rendert einen Icon-Button mit dem Label „IBAN kopieren“. Auf dem Desktop wechselt sein Tooltip zu „Copied“; auf dem Smartphone geben das Häkchen, eine kurze Vibration und eine Ansage für Screenreader das Feedback, und die kleine Größe behält auf Touch-Geräten eine Trefferfläche von 44px. Die gruppierte IBAN bricht an ihren Leerzeichen um und schiebt die Zeile so nie über die Breite eines 360px-Bildschirms hinaus.

## Heißt eine gültige IBAN, dass das Konto existiert?

Nein. Die Prüfziffern beweisen, dass die IBAN korrekt aufgebaut ist. Ein geschlossenes Konto, ein Konto bei einer inzwischen fusionierten Bank und das Konto der falschen Person bestehen den Check alle. Den letzten Fall fängt der Name des Kontoinhabers ab. Nach der EU-Verordnung über Sofortzahlungen müssen Banken die Empfängerüberprüfung (Verification of Payee) anbieten. Sie vergleicht vor einer Überweisung den Namen des Empfängers mit dem Konto und liefert ein Ergebnis wie Übereinstimmung, teilweise Übereinstimmung oder keine Übereinstimmung. Die [Übersicht der EZB zur Verordnung](https://www.ecb.europa.eu/paym/retail/instant_payments/html/instant_payments_regulation.en.html) nennt den 9. Oktober 2025 für den Euroraum und den 9. Juli 2027 für die Mitgliedstaaten ohne Euro, Rumänien eingeschlossen.

Dieser Check läuft bei der Bank des Zahlers, wenn das Geld abgeht, lange nachdem dein Formular abgeschickt wurde. Prüf also die Ziffern, während die Leute tippen, und frag neben der IBAN nach dem Namen des Kontoinhabers, wie es das Formular oben tut. Dann hat die Bank einen Namen zum Vergleichen.

## Sollte ich stattdessen eine Bibliothek wie ibantools nehmen?

Nimm eine, wenn du IBANs aus allen Ländern annimmst. Stand Oktober 2026 bringt [ibantools](https://www.npmjs.com/package/ibantools) 4.5.4 (veröffentlicht im April 2026, MIT oder MPL-2.0) die Länge und das Kontomuster jedes Registerlandes mit, `validateIBAN` liefert Fehlercodes, die du auf Meldungen abbilden kannst, und `isValidBIC` prüft SWIFT-Codes. Es prüft auch die nationalen Prüfziffern, die manche Länder im Kontoteil ergänzen, etwa Belgien, Spanien und Frankreich. Die Datei oben lässt diese aus. Ein Haken für ein SEPA-Formular: `isSEPACountry` liefert in 4.5.4 für Albanien, Moldau, Montenegro, Nordmazedonien und Serbien noch false. Das Paket [iban](https://www.npmjs.com/package/iban), die andere übliche Antwort, wurde zuletzt im Februar 2020 veröffentlicht. Für ein reines SEPA-Formular deckt die Datei oben Form, Länge und mod 97 ohne Abhängigkeit ab, und du kannst jede Zeile davon lesen.

## Sollte ich diesen Code kopieren oder installieren?

Kopier ihn, wenn du ein einziges einfaches Feld brauchst: `lib/iban.ts`, die zod-Regel und der kostenlose Input reichen dafür. In Wingo UI Pro ist Masked Input das Feld aus der Demo, und die Lib [invoicing](https://wingo-ui.com/components/invoicing) exportiert `isValidIban` (Längen für 19 Länder, jedes andere Land bekommt Form und mod 97), `formatIban`, `getIbanBank` und eine Tabelle `RO_BANKS` mit 19 rumänischen Bankcodes, neben den Checks aus unserem [Guide zur rumänischen CUI-Validierung](https://wingo-ui.com/de/blog/romanian-cui-validation-javascript) und unserem [rumänischen CNP-Validator](https://wingo-ui.com/de/blog/romanian-cnp-validator-javascript). Die Pläne findest du auf der [Preisseite](https://wingo-ui.com/de/pricing). Installier das Feld und den kostenlosen Copy-Button mit `npx wingo-ui@latest add masked-input copy-button`. Für den Rest der Formular-Anatomie lies [React Form Components: barrierefreie Felder, zod und Mobile](https://wingo-ui.com/de/blog/react-form-components-guide) oder stöber in weiteren [Komponenten-Guides](https://wingo-ui.com/de/blog/category/components).

## Komponenten in diesem Artikel

- [Masked Input](https://wingo-ui.com/components/masked-input) (Pro): A React masked input that formats itself as you type or paste: card number, expiry, CVC, IBAN, CNP, postal code or a custom mask. Installation: `npx wingo-ui@latest add masked-input`
- [Invoicing](https://wingo-ui.com/components/invoicing) (Pro): Invoice math for React billing apps: VAT per rate the way e-Factura checks it, Romanian VAT rates, and CUI, IBAN and CNP validation. Installation: `npx wingo-ui@latest add invoicing`
- [Copy Button](https://wingo-ui.com/components/copy-button) (Free): A React copy to clipboard button for IBANs, tax IDs, links and API keys: an icon button with a tooltip, a labelled button and CopyText. Installation: `npx wingo-ui@latest add copy-button`
- [Input](https://wingo-ui.com/components/input) (Free): A React text input: icons and addons inside the box, a clear button, a loading spinner, a character counter and floating labels. Installation: `npx wingo-ui@latest add input`
- [Form validation](https://wingo-ui.com/components/form-validation) (Pro): Zod validation rules for React forms with one message set (English or Romanian), react-hook-form glue and a pretend request. Installation: `npx wingo-ui@latest add form-validation`

## Häufige Fragen

### Wie prüfe ich eine IBAN in JavaScript?

Entferne Leerzeichen und Bindestriche, wandle den String in Großbuchstaben um und prüf die Länge für das Land. Dann verschiebst du die ersten vier Zeichen ans Ende, ersetzt jeden Buchstaben durch seine Zahl (A ist 10, Z ist 35) und berechnest den Rest mod 97 Ziffer für Ziffer; die IBAN ist gültig, wenn der Rest 1 ist.

### Wie sieht der Regex für eine IBAN aus?

Nachdem du die Leerzeichen entfernt und alles in Großbuchstaben umgewandelt hast, passt /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/ auf die Form jeder IBAN, und ein Länder-Pattern wie ^RO\d{2}[A-Z]{4}[A-Z0-9]{16}$ prüft zusätzlich die Länge. Keiner von beiden testet die Prüfziffern, also lass nach dem Regex noch mod 97 laufen.

### Wie viele Zeichen hat eine rumänische IBAN?

24: RO, zwei Prüfziffern, vier Buchstaben für die Bank (die ersten vier Zeichen ihres BIC) und 16 Buchstaben oder Ziffern für Filiale und Konto, zum Beispiel RO49 AAAA 1B31 0075 9384 0000.

### Heißt eine gültige IBAN, dass das Konto existiert?

Nein. Die Prüfziffern beweisen nur, dass die IBAN korrekt aufgebaut ist, also besteht auch ein geschlossenes Konto oder das Konto der falschen Person den Test. In der EU vergleicht die Empfängerüberprüfung (Verification of Payee) bei der Bank des Zahlers den Namen des Empfängers mit dem Konto, bevor eine Überweisung ausgeführt wird, ab dem 9. Oktober 2025 im Euroraum und ab dem 9. Juli 2027 in Rumänien und den anderen Mitgliedstaaten ohne Euro.

### Warum lehnt mein IBAN-Check gültige IBANs ab?

Oft liegt es an Number oder parseInt: Die umgestellte IBAN hat mehr Ziffern, als der Typ Number in JavaScript exakt darstellen kann, also kommt ein falscher Rest heraus. Kleinbuchstaben, aus einem PDF eingefügte geschützte Leerzeichen und ein vorangestelltes „IBAN:“ sind die anderen üblichen Verdächtigen.

### Wie finde ich die Bank zu einer rumänischen IBAN?

Lies die Zeichen 5 bis 8. Sie sind die ersten vier Zeichen des BIC der Bank, also steht BTRL für Banca Transilvania und RNCB für BCR. Halte deine Tabelle aktuell, denn fusionierte Banken hinterlassen alte Codes.

---

Quelle: https://wingo-ui.com/de/blog/iban-validation-javascript
