{"id":2098,"date":"2026-08-15T19:56:00","date_gmt":"2026-08-15T18:56:00","guid":{"rendered":"https:\/\/cerovac.com\/a11y\/?p=2098"},"modified":"2026-08-10T12:58:30","modified_gmt":"2026-08-10T11:58:30","slug":"when-a-disabled-submit-button-becomes-a-dead-end","status":"publish","type":"post","link":"https:\/\/cerovac.com\/a11y\/2026\/08\/when-a-disabled-submit-button-becomes-a-dead-end\/","title":{"rendered":"When a disabled submit button becomes a dead end"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">I&#8217;ve been using a password manager for some years and sometimes I also use the &#8220;use a securely generated password&#8221; function in it or in my browser. And I&#8217;ve noticed that when forms have their submit buttons disabled and we use this feature it often does not enable the button at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Skip to <a href=\"#demo-forms\" data-type=\"internal\" data-id=\"#demo-forms\">demo at the end<\/a> if you want to test it for yourself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Now, while this is not a directly failure of WCAG &#8211; <a href=\"https:\/\/www.w3.org\/WAI\/WCAG22\/Understanding\/accessible-authentication-minimum.html#login-forms\" target=\"_blank\" rel=\"noreferrer noopener\">success criterion 3.3.8 Accessible Authentication (Minimum, Level AA) comes to mind (opens in new window)<\/a>, <strong>I am definitely flagging this as usability issue<\/strong> in <a href=\"https:\/\/cerovac.com\/a11y\/accessibility-services\/\" data-type=\"page\" data-id=\"1513\">my accessibility and usability audits<\/a>.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\">However, if user agents and password managers are actively blocked from filling in the fields (for instance, by a script that prevents form fields from being automatically populated), or users are prevented from copy and paste operations (as they may rely on standalone\/external third party password managers), then the page would fail this criterion unless an alternative is provided.<\/p>\n<cite>WCAG 2.2, success criterion 3.3.8 Accessible Authentication (Minimum, Level AA) on login forms is relevant if page is actively preventing the password manager, but <strong>has not flagged the disabled button<\/strong> related issue I am writing about.<\/cite><\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\">I have made my share of forms and am still making them. I have used the disabled button pattern a lot of times in the past, mostly by the instruction of the designers. Sometimes because I was lazy and I didn&#8217;t want to add the user experience logic to inform the user about the issues even when form was not valid.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So I know that <strong>disabled buttons are just a sign of being lazy and being fast<\/strong>. If you use AI for coding and you got a disabled button solution like this &#8211; you AI is reeealy lazy as well and I recommend teaching it with some persistent instructions or skills to never use the disabled button pattern again!<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But if you really can&#8217;t go against it and a disabled button is kind of your only option, we can perhaps still do something to improve the experience just a bit.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">If there is no other way and the button really needs to be disabled<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Sometimes we still have to live with some solutions that are out of our hands to improve or change a lot. In this case, if we can, we should at least try to improve the user experience.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">If your hands are tied &#8211; at least explain in text<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>As a minimum, I suggest at least informing the users in text, before the form.<\/strong> As some assistive technology basically removes the disabled buttons it may be beneficial to at least know about it. &#8220;Submit button is disabled until form is valid&#8221; can mean a big difference for some people.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">If you can change the JavaScript<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">If we have the access to JavaScript for the form, but still need to use the disabled button, then make sure fields are listening to <code>input<\/code> events, and not only <code>keyup<\/code> and similar ones.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The &#8220;use a securely generated password&#8221; function does not trigger a <code>keyup<\/code>. That makes the form kind of valid, visually, but the script has not detected the populated values and considers the fields empty. And disabled button is therefore still disabled. And user has no clue &#8211; it seems that the form is broken as it does not react to the input.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Definitely test with different password managers or at least different browsers that offer this solution (Firefox does and many others as well).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Or &#8211; just go against the disabled button and at least show an old school <code>alert<\/code> message like it is 1995. It works way better than when people guess and struggle and probably just go away if they can.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Disabled buttons should be a thing of the past<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">There are some border scenarios where a disabled button is viable. Like Previous button in pagination where we are already on the first page, or a quantity stepper at its minimum and so on. If we can&#8217;t just hide the button for visual harmony, we can use the <code>aria-disabled=\"true\"<\/code> instead of <code>disabled<\/code> HTML attribute, to at least allow tabbing to it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">But I guess that in most cases it is better to leave it active and inform the users properly. It often depends on the context as usual.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So I really hope that at least disabling submit buttons based on validation rules is soon a thing of the past.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"demo-forms\" tabindex=\"-1\">Demo forms<\/h2>\n<style>\n#demo-forms:focus-visible{\noutline: 1px solid;\noutline-offset: 2px;\n}\n<\/style>\n\n\n\n<p class=\"wp-block-paragraph\">These forms do not send any data anywhere<strong> and are not production ready in any way!<\/strong> <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">They only demonstrate the bad (Form 1), better (Form 2) and even better (Form 3) user experience of the submit button &#8211; and allow you to test with your browser or password manager to populate passwords automatically and then toggle the disabled state (with form 1 and 2).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Forms 1 and 2 disable the submit button until the two passwords match. <strong>Try each one twice: once by typing, and once using your browser&#8217;s &#8220;suggest strong password&#8221; \/ autofill.<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Form 3 does not disable the submit button &#8211; it provides feedback in all cases.<\/p>\n\n\n\n<style data-wp-block-html=\"css\">\n.demo fieldset {\n  border: 1px solid;\n  border-radius: .5rem;\n  padding: 1rem 1.25rem 1.25rem;\n  margin-block-end: 2rem;\n}\n\n.demo legend { font-weight: 700; padding-inline: .4rem; }\n\n.demo .field { margin-block-end: 1rem; }\n\n.demo label { display: block; font-weight: 600; margin-block-end: .25rem; }\n\n.demo input[type=\"password\"] {\n  width: 100%;\n  box-sizing: border-box;\n  padding: .5rem;\n  font: inherit;\n  border: 1px solid;\n  border-radius: .3rem;\n}\n\n.demo input:focus-visible { outline: 3px solid; outline-offset: 1px; }\n\n.demo .status {\n  font-weight: 700;\n  margin-block: .5rem 1rem;\n  min-height: 1.5em;\n}\n\n.demo button[type=\"submit\"] {\n  font: inherit;\n  padding: .5rem 1rem;\n  border-radius: .3rem;\n  cursor: pointer;\n}\n\n.demo button[type=\"submit\"]:disabled { cursor: not-allowed; }\n<\/style>\n\n<div class=\"demo\">\n\n  <form id=\"form-keyup\" novalidate=\"\">\n    <fieldset>\n      <legend>Form 1 (bad) \u2014 listens to <code>keyup<\/code> (breaks on autofill)<\/legend>\n\n      <div class=\"field\">\n        <label for=\"pw1\">New password<\/label>\n        <input type=\"password\" id=\"pw1\" data-role=\"password\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <div class=\"field\">\n        <label for=\"confirm1\">Confirm password<\/label>\n        <input type=\"password\" id=\"confirm1\" data-role=\"confirm\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <p class=\"status\" role=\"status\" aria-live=\"polite\" data-role=\"status\"><\/p>\n\n      <button type=\"submit\" disabled=\"\">Submit<\/button>\n    <\/fieldset>\n  <\/form>\n\n  <form id=\"form-input\" novalidate=\"\">\n    <fieldset>\n      <legend>Form 2 (better) \u2014 listens to <code>input<\/code> (works on autofill)<\/legend>\n\n      <div class=\"field\">\n        <label for=\"pw2\">New password<\/label>\n        <input type=\"password\" id=\"pw2\" data-role=\"password\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <div class=\"field\">\n        <label for=\"confirm2\">Confirm password<\/label>\n        <input type=\"password\" id=\"confirm2\" data-role=\"confirm\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <p class=\"status\" role=\"status\" aria-live=\"polite\" data-role=\"status\"><\/p>\n\n      <button type=\"submit\" disabled=\"\">Submit<\/button>\n    <\/fieldset>\n  <\/form>\n\n  <form id=\"form-alert\" novalidate=\"\">\n    <fieldset>\n      <legend>Form 3 \u2014 <code>keyup<\/code>, button never disabled, checks on submit<\/legend>\n\n      <div class=\"field\">\n        <label for=\"pw3\">New password<\/label>\n        <input type=\"password\" id=\"pw3\" data-role=\"password\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <div class=\"field\">\n        <label for=\"confirm3\">Confirm password<\/label>\n        <input type=\"password\" id=\"confirm3\" data-role=\"confirm\" autocomplete=\"new-password\" required=\"\">\n      <\/div>\n\n      <p class=\"status\" role=\"status\" aria-live=\"polite\" data-role=\"status\"><\/p>\n\n      <button type=\"submit\">Submit<\/button>\n    <\/fieldset>\n  <\/form>\n\n<script>\nfunction wireForm(form, eventName) {\n      const password = form.querySelector('[data-role=\"password\"]');\n      const confirm  = form.querySelector('[data-role=\"confirm\"]');\n      const submit   = form.querySelector('button[type=\"submit\"]');\n      const status   = form.querySelector('[data-role=\"status\"]');\n\n      function check() {\n        const bothFilled = password.value.length > 0 && confirm.value.length > 0;\n        const matches = bothFilled && password.value === confirm.value;\n\n        submit.disabled = !matches;\n\n        if (!bothFilled) {\n          status.textContent = \"\";\n        } else if (matches) {\n          status.textContent = \"\\u2713 Passwords match\";\n        } else {\n          status.textContent = \"\\u2717 Passwords do not match yet\";\n        }\n      }\n\n      password.addEventListener(eventName, check);\n      confirm.addEventListener(eventName, check);\n\n      form.addEventListener(\"submit\", function (event) {\n        event.preventDefault();\n        status.textContent = \"\\u2713 Submitted (demo \\u2014 nothing was sent)\";\n      });\n    }\n\n    function wireAlertForm(form, eventName) {\n      const password = form.querySelector('[data-role=\"password\"]');\n      const confirm  = form.querySelector('[data-role=\"confirm\"]');\n      const status   = form.querySelector('[data-role=\"status\"]');\n\n      function updateStatus() {\n        const bothFilled = password.value.length > 0 && confirm.value.length > 0;\n        const matches = bothFilled && password.value === confirm.value;\n        if (!bothFilled) {\n          status.textContent = \"\";\n        } else if (matches) {\n          status.textContent = \"\\u2713 Passwords match\";\n        } else {\n          status.textContent = \"\\u2717 Passwords do not match yet\";\n        }\n      }\n\n      password.addEventListener(eventName, updateStatus);\n      confirm.addEventListener(eventName, updateStatus);\n\n      form.addEventListener(\"submit\", function (event) {\n        event.preventDefault();\n        if (password.value.length > 0 && password.value === confirm.value) {\n          alert(\"\\u2713 Passwords match \\u2014 form would submit.\");\n        } else {\n          alert(\"\\u2717 Passwords do not match.\");\n        }\n      });\n    }\n\n    wireForm(document.getElementById(\"form-keyup\"), \"keyup\");\n    wireForm(document.getElementById(\"form-input\"), \"input\");\n    wireAlertForm(document.getElementById(\"form-alert\"), \"keyup\");\n<\/script>\n\n<\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Forms 1 and 2 differ only in the JavaScript event name passed to <code>wireForm()<\/code>. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Form 3 keeps the <code>keyup<\/code> event (best-effort, still misses autofill) but never disables the button on it \u2014 the real check runs on submit, reading the current field values, so how they were filled is irrelevant.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">So &#8211; once again &#8211; you do not need to disable the button, but if you really must &#8211; make sure it works with all modes a user can use the form!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Manually typing passwords is a burden and is slowly being abstracted or going away. Disabled buttons for sign up, login, password resets are often a lazy solution with giant possibility to block users out. Do not use disabled buttons please.<\/p>\n","protected":false},"author":2,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[123,94,10],"tags":[927,438,926,74,251,928,129,352,29],"class_list":["post-2098","post","type-post","status-publish","format-standard","hentry","category-accessibility-testing","category-code-examples","category-practical-a11y","tag-aria-disabled","tag-authentication","tag-disabled-button","tag-html","tag-javascript","tag-password-manager","tag-usability","tag-user-experience","tag-wcag"],"_links":{"self":[{"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/posts\/2098","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/comments?post=2098"}],"version-history":[{"count":3,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/posts\/2098\/revisions"}],"predecessor-version":[{"id":2105,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/posts\/2098\/revisions\/2105"}],"wp:attachment":[{"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/media?parent=2098"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/categories?post=2098"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cerovac.com\/a11y\/wp-json\/wp\/v2\/tags?post=2098"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}