diff --git a/dictionaries/privacyidea.definition.json b/dictionaries/privacyidea.definition.json
index df58b6f5cc0baea5780351678b1941db17b573a0..a762ff495955526ff0b62ca16d84667f61e337be 100644
--- a/dictionaries/privacyidea.definition.json
+++ b/dictionaries/privacyidea.definition.json
@@ -35,7 +35,7 @@
   "login_text_challenge": {
     "en": "You entered the correct PIN or password. But now you also need to use your second factor."
   },
-  "scanTokenQR": {
+  "scan_token_qr": {
     "en": "Scan this QR code with an app like Google Authenticator or privacyIDEA Authenticator and enter the displayed code below."
   },
   "chal_resp_message": {
@@ -44,31 +44,49 @@
   "enroll_u2f": {
     "en": "Please use your u2f device to finish the enrollment process"
   },
-  "u2fNotWorking": {
-    "en": "U2F is currently not working."
-  },
   "alternate_login_options": {
     "en": "Alternate login options:"
   },
-  "alert_webauthn_insecure_context": {
+  "webauthn_insecure_context": {
     "en": "Unable to proceed with Web Authn because the context is insecure!"
   },
-  "alert_webauthn_unavailable": {
+  "webauthn_library_unavailable": {
     "en": "Could not load WebAuthn, your device probably does not support it. Please try again or use another method."
   },
-  "alert_webAuthnSignRequest_error": {
-    "en": "Error while signing WebAuthnSignRequest:"
+  "webauthn_AbortError": {
+    "en": "You have canceled the authentication."
+  },
+  "webauthn_InvalidStateError": {
+    "en": "You have used an unregistered WebAuthn device. Please use a different one."
+  },
+  "webauthn_NotAllowedError": {
+    "en": "WebAuthn is disabled for this site."
+  },
+  "webauthn_NotSupportedError": {
+    "en": "Your device does not support WebAuthn features that we need."
+  },
+  "webauthn_TypeError": {
+    "en": "There is a problem with the WebAuthn parameters that we use."
+  },
+  "webauthn_other_error": {
+    "en": "There was a problem communicating with your device."
+  },
+  "webauthn_in_progress": {
+    "en": "Trying to communicate with your WebAuthn device. Plug it in and press the button or confirm the system dialog."
+  },
+  "webauthn_success": {
+    "en": "We heard back from your WebAuthn device. You have been authenticated."
   },
-  "alert_u2f_insecure_context": {
+  "u2f_insecure_context": {
     "en": "Unable to proceed with U2F because the context is insecure!"
   },
-  "alert_u2f_unavailable": {
+  "u2f_unavailable": {
     "en": "Could not load U2F, your device probably does not support it. Please try again or use another method."
   },
-  "alert_U2FSignRequest_error": {
+  "u2f_sign_request_error": {
     "en": "Error while signing U2FSignRequest:"
   },
-  "tryAgain": {
+  "try_again": {
     "en": "Try Again"
   },
   "error_message": {
diff --git a/dictionaries/privacyidea.translation.json b/dictionaries/privacyidea.translation.json
index cebf65a2aaf45f78b43a5073376ebb368cd373b7..a1d528c67c09c136205230bd81695751bb44b563 100644
--- a/dictionaries/privacyidea.translation.json
+++ b/dictionaries/privacyidea.translation.json
@@ -54,7 +54,7 @@
     "de": "Sie haben die korrekte PIN, bzw. das korrekte Passwort eingegeben. Aber nun mĂĽssen Sie noch Ihren zweiten Faktor nutzen.",
     "nl": "U heeft het de juiste PIN of wachtwoord ingegegeven. Geef nu ook uw tweede factor in."
   },
-  "scanTokenQR": {
+  "scan_token_qr": {
     "cs": "Naskenujte tento QR kĂłd aplikacĂ­ pro TOTP (napĹ™. Google Authenticator nebo privacyIDEA Authenticator) a zobrazenĂ˝ kĂłd vloĹľte nĂ­Ĺľe.",
     "de": "Scannen Sie den QR-Code mit einer App, wie dem Google Authenticator oder privacyIDEA Authenticator und geben Sie den angezeigten Code unten ein.",
     "nl": "Scan deze QR-code met een app zoals Google Authenticator of privacyIDEA Authenticator en geef de getoonde code in."
@@ -69,34 +69,50 @@
     "de": "Bitte nutzen Sie Ihr U2F-Gerät, um das Ausrollen fertigzustellen.",
     "nl": "Gebruik uw u2f-toestel om de aanmelding verder te zetten."
   },
-  "u2fNotWorking": {
-    "cs": "U2F nenĂ­ k dispozici.",
-    "de": "U2F funktioniert momentan nicht.",
-    "nl": "U2F werkt momenteel niet."
-  },
   "alternate_login_options": {
     "cs": "Další možnosti přihlášení:",
     "de": "Alternative Login Optionen:"
   },
-  "alert_webauthn_insecure_context": {
+  "webauthn_insecure_context": {
     "cs": "Není možné použít autentizaci WebAuthn, protože neodpovídá bezpečnostní kontext."
   },
-  "alert_webauthn_unavailable": {
+  "webauthn_library_unavailable": {
     "cs": "Načtení WebAuthn se nezdařilo, nejspíš ho Vaše zařízení nepodporuje. Zkuste to znovu nebo použijte jinou metodu."
   },
-  "alert_webAuthnSignRequest_error": {
-    "cs": "Chyba při podepisování WebAuthnSignRequest:"
+  "webauthn_AbortError": {
+    "cs": "Ověření WebAuthn bylo zrušeno."
+  },
+  "webauthn_NotAllowedError": {
+    "cs": "WebAuthn není pro tuto stránku povoleno."
+  },
+  "webauthn_InvalidStateError": {
+    "cs": "Použité WebAuthn zařízení nemáte zaregistrované, prosím použijte jiné."
+  },
+  "webauthn_NotSupportedError": {
+    "cs": "Vaše zařízení nepodporuje možnosti WebAuthn, které potřebujeme."
+  },
+  "webauthn_TypeError": {
+    "cs": "WebAuthn parametry jsou nesprávné."
+  },
+  "webauthn_other_error": {
+    "cs": "Komunikace s Vaším WebAuthn zařízením se nezdařila."
+  },
+  "webauthn_in_progress": {
+    "cs": "Probíhá komunikace s Vaším WebAuthn zařízením. Připojte fyzické zařízení a zmáčkněte tlačítko nebo potvrďte systémový dialog."
+  },
+  "webauthn_success": {
+    "cs": "Obdrželi jsme odpověď z Vašeho WebAuthn zařízení. Autentizace byla úspěšná."
   },
-  "alert_u2f_insecure_context": {
+  "u2f_insecure_context": {
     "cs": "Není možné použít autentizaci U2F, protože neodpovídá bezpečnostní kontext."
   },
-  "alert_u2f_unavailable": {
+  "u2f_unavailable": {
     "cs": "Načtení U2F se nezdařilo, nejspíš ho Vaše zařízení nepodporuje. Zkuste to znovu nebo použijte jinou metodu."
   },
-  "alert_U2FSignRequest_error": {
+  "u2f_sign_request_error": {
     "cs": "Chyba při podepisování U2FSignRequest:"
   },
-  "tryAgain": {
+  "try_again": {
     "cs": "Zkusit znovu",
     "de": "Nochmal versuchen",
     "nl": "Probeer opnieuw"
diff --git a/templates/LoginForm.php b/templates/LoginForm.php
index 81dd63ef29200fa83776b3d26332172163fe1e22..988be5682e64509895a8e7caa52a71998473764d 100644
--- a/templates/LoginForm.php
+++ b/templates/LoginForm.php
@@ -93,14 +93,15 @@ if (null !== $this->data['errorCode']) {
                                     <?php
                                 } else {
                                     ?>
-                                    <label for="username" class="sr-only">
-                                        <?php echo $this->t('{login:username}'); ?>
-                                    </label>
-                                    <input type="text" id="username" tabindex="1" name="username" autofocus
-                                           value="<?php echo htmlspecialchars($this->data['username'], ENT_QUOTES); ?>"
-                                           placeholder="<?php echo htmlspecialchars($this->t('{login:username}'), ENT_QUOTES); ?>"
-                                    />
-                                    <br>
+                                    <p>
+                                        <label for="username" class="sr-only">
+                                            <?php echo $this->t('{login:username}'); ?>
+                                        </label>
+                                        <input type="text" id="username" tabindex="1" name="username" autofocus
+                                               value="<?php echo htmlspecialchars($this->data['username'], ENT_QUOTES); ?>"
+                                               placeholder="<?php echo htmlspecialchars($this->t('{login:username}'), ENT_QUOTES); ?>"
+                                        />
+                                    </p>
                                     <?php
                                 }
 
@@ -137,7 +138,7 @@ if (null !== $this->data['errorCode']) {
                                 <input id="password" name="password" tabindex="1" type="password" value="" class="text"
                                        placeholder="<?php echo htmlspecialchars($passHint, ENT_QUOTES); ?>"/>
 
-                                <strong id="message"><?php
+                                <p id="message" role="alert"><?php
                                     $messageOverride = $this->data['messageOverride'] ?? null;
                                     if (null === $messageOverride || is_string($messageOverride)) {
                                         echo htmlspecialchars(
@@ -147,18 +148,20 @@ if (null !== $this->data['errorCode']) {
                                     } elseif (is_callable($messageOverride)) {
                                         echo call_user_func($messageOverride, $this->data['message'] ?? '');
                                     }
-                                ?></strong>
-                                <br><br>
-                                <label for="otp" class="sr-only">
-                                    <?php echo $this->t('{privacyidea:privacyidea:otp}'); ?>
-                                </label>
-                                <input id="otp" name="otp" type="password"
-                                       placeholder="<?php echo htmlspecialchars($otpHint, ENT_QUOTES); ?>">
-                                <br><br>
-                                <input id="submitButton" tabindex="1" class="rc-button rc-button-submit" type="submit"
-                                       name="Submit"
-                                       value="<?php echo htmlspecialchars($this->t('{login:login_button}'), ENT_QUOTES); ?>"/>
-                                <br><br>
+                                ?></p>
+
+                                <p>
+                                    <label for="otp" class="sr-only">
+                                        <?php echo $this->t('{privacyidea:privacyidea:otp}'); ?>
+                                    </label>
+                                    <input id="otp" name="otp" type="password" placeholder="<?php echo htmlspecialchars($otpHint, ENT_QUOTES); ?>">
+                                </p>
+
+                                <p>
+                                    <button id="submitButton" tabindex="1" class="rc-button rc-button-submit" type="submit" name="Submit" value="1">
+                                        <?php echo htmlspecialchars($this->t('{login:login_button}'), ENT_QUOTES); ?>
+                                    </button>
+                                </p>
 
                                 <!-- Undefined index is suppressed and the default is used for these values -->
                                 <input id="mode" type="hidden" name="mode"
@@ -193,7 +196,7 @@ if (null !== $this->data['errorCode']) {
                                 <?php
                                 // If enrollToken load QR Code
                                 if (isset($this->data['tokenQR'])) {
-                                    echo htmlspecialchars($this->t('{privacyidea:privacyidea:scanTokenQR}')); ?>
+                                    echo htmlspecialchars($this->t('{privacyidea:privacyidea:scan_token_qr}')); ?>
                                     <div class="tokenQR">
                                         <?php echo '<img src="' . $this->data['tokenQR'] . '" />'; ?>
                                     </div>
@@ -297,8 +300,20 @@ if (!empty($this->data['links'])) {
     <meta id="privacyidea-translations" name="privacyidea-translations" content="<?php
     $translations = [];
     $translation_keys = [
-        'alert_webauthn_insecure_context', 'alert_webauthn_unavailable', 'alert_webAuthnSignRequest_error',
-        'alert_u2f_insecure_context', 'alert_u2f_unavailable', 'alert_U2FSignRequest_error',
+        'webauthn_insecure_context',
+        'webauthn_library_unavailable',
+        'webauthn_AbortError',
+        'webauthn_InvalidStateError',
+        'webauthn_NotAllowedError',
+        'webauthn_NotSupportedError',
+        'webauthn_TypeError',
+        'webauthn_other_error',
+        'webauthn_in_progress',
+        'webauthn_success',
+        'u2f_insecure_context',
+        'u2f_unavailable',
+        'u2f_sign_request_error',
+        'try_again',
     ];
     foreach ($translation_keys as $translation_key) {
         $translations[$translation_key] = $this->t(sprintf('{privacyidea:privacyidea:%s}', $translation_key));
diff --git a/www/js/loginform.js b/www/js/loginform.js
index 4f8f1362dbc2c2b7dc87d2b3bc3e18cacabe17f3..4d55780b321f4402f439cb126a5ebbce2d4821dc 100644
--- a/www/js/loginform.js
+++ b/www/js/loginform.js
@@ -24,109 +24,92 @@ function set(id, value) {
   }
 }
 
-function disable(id) {
+function hide(id) {
   const element = getElement(id);
   if (element != null) {
     element.classList.add("hidden");
   }
 }
 
-function enable(id) {
+function show(id) {
   const element = getElement(id);
   if (element != null) {
     element.classList.remove("hidden");
   }
 }
 
-function changeMode(newMode) {
-  set("mode", newMode);
-  set("modeChanged", "true");
-  document.forms["piLoginForm"].submit();
-}
-
-function t(key) {
-  return JSON.parse(getContent("privacyidea-translations"))[key];
-}
-
-const step = getContent("privacyidea-step");
-
-if (step > "1") {
-  disable("username");
-  disable("password");
-} else {
-  disable("otp");
-  disable("message");
-  disable("AlternateLoginOptions");
-}
-
-// Set alternate token button visibility
-if (value("webAuthnSignRequest") === "") {
-  disable("useWebAuthnButton");
-}
-
-if (value("u2fSignRequest") === "") {
-  disable("useU2FButton");
-}
-
-if (!booleanValue("pushAvailable")) {
-  disable("usePushButton");
+function disable(id) {
+  const element = getElement(id);
+  if (element != null) {
+    element.disabled = true;
+    element.classList.add("disabled");
+  }
 }
 
-if (!booleanValue("otpAvailable")) {
-  disable("useOTPButton");
+function enable(id) {
+  const element = getElement(id);
+  if (element != null) {
+    element.disabled = false;
+    element.classList.remove("disabled");
+  }
 }
 
-if (
-  !booleanValue("pushAvailable") &&
-  value("webAuthnSignRequest") === "" &&
-  value("u2fSignRequest") === ""
-) {
-  disable("alternateTokenDiv");
+function changeMode(newMode) {
+  set("mode", newMode);
+  set("modeChanged", "true");
+  document.forms["piLoginForm"].submit();
 }
 
-if (value("mode") === "otp") {
-  disable("useOTPButton");
+function setMessage(newMessage) {
+  getElement("message").innerText = newMessage;
 }
 
-if (value("mode") === "webauthn") {
-  disable("otp");
-  disable("submitButton");
-  doWebAuthn();
+function t(key) {
+  return JSON.parse(getContent("privacyidea-translations"))[key];
 }
 
-if (value("mode") === "u2f") {
-  disable("otp");
-  disable("submitButton");
-  doU2F();
+function getWebAuthnErrorMessage(err) {
+  switch (err.name) {
+    case "AbortError":
+    case "InvalidStateError":
+    case "NotAllowedError":
+    case "NotSupportedError":
+    case "TypeError":
+      return t("webauthn_" + err.name);
+    default:
+      return t("webauthn_other_error") + " (" + err.name + ")";
+  }
 }
 
-if (value("mode") === "push") {
-  const pollingIntervals = [4, 3, 2, 1];
-
-  disable("otp");
-  disable("usePushButton");
-  disable("submitButton");
-
-  if (value("loadCounter") > pollingIntervals.length - 1) {
-    refreshTime = pollingIntervals[pollingIntervals.length - 1];
+function webAuthnError(err) {
+  const errorMessage = getWebAuthnErrorMessage(err);
+  console.log("Error while signing WebAuthnSignRequest: " + err);
+  enable("useWebAuthnButton");
+  setMessage(errorMessage);
+  if (getElement("retryWebAuthnButton")) {
+    show("retryWebAuthnButton");
   } else {
-    refreshTime = pollingIntervals[Number(value("loadCounter") - 1)];
+    const retryWebAuthnButton = getElement("useWebAuthnButton").cloneNode();
+    retryWebAuthnButton.addEventListener("click", doWebAuthn);
+    retryWebAuthnButton.id = "retryWebAuthnButton";
+    retryWebAuthnButton.innerHTML = "<span>" + t("try_again") + "</span>";
+    getElement("message").innerHTML += " ";
+    getElement("message").appendChild(retryWebAuthnButton);
   }
-
-  refreshTime *= 1000;
-  setTimeout(() => {
-    document.forms["piLoginForm"].submit();
-  }, refreshTime);
 }
 
 function doWebAuthn() {
+  disable("useWebAuthnButton");
+  hide("retryWebAuthnButton");
+  setMessage(t("webauthn_in_progress"));
   // If mode is push, we have to change it, otherwise the site will refresh while doing webauthn
   if (value("mode") === "push") {
     changeMode("webauthn");
   }
 
   if (!window.isSecureContext) {
-    alert(t("alert_webauthn_insecure_context"));
+    enable("useWebAuthnButton");
+    setMessage(t("webauthn_insecure_context"));
     console.log(
       "Insecure context detected: Aborting Web Authn authentication!"
     );
@@ -135,7 +118,8 @@ function doWebAuthn() {
   }
 
   if (!window.pi_webauthn) {
-    alert(t("alert_webauthn_unavailable"));
+    enable("useWebAuthnButton");
+    setMessage(t("webauthn_library_unavailable"));
     changeMode("otp");
     return;
   }
@@ -156,15 +140,17 @@ function doWebAuthn() {
     const requestjson = JSON.parse(requestStr);
 
     const webAuthnSignResponse = window.pi_webauthn.sign(requestjson);
-    webAuthnSignResponse.then((webauthnresponse) => {
-      const response = JSON.stringify(webauthnresponse);
-      set("webAuthnSignResponse", response);
-      set("mode", "webauthn");
-      document.forms["piLoginForm"].submit();
-    });
+    webAuthnSignResponse
+      .then((webauthnresponse) => {
+        const response = JSON.stringify(webauthnresponse);
+        set("webAuthnSignResponse", response);
+        set("mode", "webauthn");
+        setMessage(t("webauthn_success"));
+        document.forms["piLoginForm"].submit();
+      })
+      .catch(webAuthnError);
   } catch (err) {
-    console.log("Error while signing WebAuthnSignRequest: " + err);
-    alert(t("alert_webAuthnSignRequest_error") + " " + err);
+    webAuthnError(err);
   }
 }
 
@@ -175,7 +161,7 @@ function doU2F() {
   }
 
   if (!window.isSecureContext) {
-    alert(t("alert_u2f_insecure_context"));
+    setMessage(t("u2f_insecure_context"));
     console.log("Insecure context detected: Aborting U2F authentication!");
     changeMode("otp");
     return;
@@ -184,7 +170,7 @@ function doU2F() {
   const requestStr = value("u2fSignRequest");
 
   if (requestStr === null) {
-    alert(t("alert_u2f_unavailable"));
+    setMessage(t("u2f_unavailable"));
     changeMode("otp");
     return;
   }
@@ -194,7 +180,7 @@ function doU2F() {
     sign_u2f_request(requestjson);
   } catch (err) {
     console.log("Error while signing U2FSignRequest: " + err);
-    alert(t("alert_U2FSignRequest_error") + " " + err);
+    setMessage(t("u2f_sign_request_error") + " " + err);
   }
 }
 
@@ -221,17 +207,93 @@ function sign_u2f_request(signRequest) {
   });
 }
 
-if (getContent("privacyidea-hide-alternate") === "true") {
-  disable("AlternateLoginOptions");
+function addClickListener(id, listener) {
+  const el = getElement(id);
+  if (el !== null) {
+    el.addEventListener("click", listener);
+  }
 }
 
-document.addEventListener("DOMContentLoaded", (event) => {
-  getElement("useWebAuthnButton").addEventListener("click", doWebAuthn);
-  getElement("usePushButton").addEventListener("click", function () {
-    changeMode("push");
-  });
-  getElement("useOTPButton").addEventListener("click", function () {
-    changeMode("otp");
-  });
-  getElement("useU2FButton").addEventListener("click", doU2F);
-});
+function initPrivacyIDEA() {
+  const step = getContent("privacyidea-step");
+
+  if (step > "1") {
+    hide("username");
+    hide("password");
+  } else {
+    hide("otp");
+    hide("message");
+    hide("AlternateLoginOptions");
+  }
+
+  // Set alternate token button visibility
+  if (value("webAuthnSignRequest") === "") {
+    hide("useWebAuthnButton");
+  }
+
+  if (value("u2fSignRequest") === "") {
+    hide("useU2FButton");
+  }
+
+  if (!booleanValue("pushAvailable")) {
+    hide("usePushButton");
+  }
+
+  if (!booleanValue("otpAvailable")) {
+    hide("useOTPButton");
+  }
+
+  if (
+    !booleanValue("pushAvailable") &&
+    value("webAuthnSignRequest") === "" &&
+    value("u2fSignRequest") === ""
+  ) {
+    hide("AlternateLoginOptions");
+  }
+
+  if (value("mode") === "otp") {
+    hide("useOTPButton");
+  }
+
+  if (value("mode") === "webauthn") {
+    hide("otp");
+    hide("submitButton");
+    doWebAuthn();
+  }
+
+  if (value("mode") === "u2f") {
+    hide("otp");
+    hide("submitButton");
+    doU2F();
+  }
+
+  if (value("mode") === "push") {
+    const pollingIntervals = [4, 3, 2, 1];
+
+    hide("otp");
+    hide("usePushButton");
+    hide("submitButton");
+
+    if (value("loadCounter") > pollingIntervals.length - 1) {
+      refreshTime = pollingIntervals[pollingIntervals.length - 1];
+    } else {
+      refreshTime = pollingIntervals[Number(value("loadCounter") - 1)];
+    }
+
+    refreshTime *= 1000;
+    setTimeout(() => {
+      document.forms["piLoginForm"].submit();
+    }, refreshTime);
+  }
+
+  if (getContent("privacyidea-hide-alternate") === "true") {
+    hide("AlternateLoginOptions");
+  }
+
+  addClickListener("useWebAuthnButton", doWebAuthn);
+  addClickListener("usePushButton", () => changeMode("push"));
+  addClickListener("useOTPButton", () => changeMode("otp"));
+  addClickListener("useU2FButton", doU2F);
+}
+
+document.addEventListener("DOMContentLoaded", initPrivacyIDEA);