<!--
  Part of CSS-Tricks blogpost. Check footer for the link

  Look for [DIFF] to know the difference between 2/3 and 3/3 versions.
  Demo 2/3: https://codepen.io/sandrina-p/pen/NWdBYXL

  Demo Extra: https://codepen.io/sandrina-p/pen/WNRYabB
-->
<div class="g-hero">
  <h1>Inclusive disabled buttons · Demo 3/3</h1>
  <p class="about">Disabled button using <code>aria-disabled</code> attribute and tooltip</p>
</div>

<div class="demo g-block">
  <form class="area js-form" novalidate>
    <div class="areaStart">
      <div class="field">
        <label for="ticketCount" class="fieldLabel"><span class="sr-only">Number of</span> Tickets</label>
        <input id="ticketCount" class="fieldInput js-tickets" type="text" pattern="[0-9]+" placeholder="0" required />
      </div>
    </div>

    <div class="areaEnd">
      <div class="tooltipArea isActive js-tooltip">

        <!-- [DIFF] - change attribute from disabled to aria-disabled -->
        <button type="submit" class="btnSubmit js-btnSubmit" aria-disabled="true" aria-describedby="disabledReason">
          <span class="btnSubmit-text">Add to cart</span>
          <span class="sr-only js-loadingMsg" aria-live="assertive" data-loading-msg="Adding to cart, wait..."></span>
        </button>

        <div role="tooltip" class="tooltipBox" id="disabledReason">
          <span class="tooltipItself">Add between 1 and 9 tickets</span>
        </div>
      </div>
      <p aria-live="assertive" class="formStatus js-feedback"></p>
    </div>
  </form>
</div>
<p class="terms">Some <a href="#" class="u-link">dummy terms</a> after.</p>

<!-- -->
<!-- -->
<!-- -->

<footer class="g-footer">
  <p>This is part of a <a href="https://css-tricks.com/making-disabled-buttons-more-inclusive" class="u-link">blog post</a> on CSS-Tricks.</p>
  <p>Made <a href="https://www.buymeacoffee.com/sandrinap" class="u-link">without coffee</a> by <a href="https://twitter.com/a_sandrina_p" class="u-link">Sandrina Pereira</a>.</p>
</footer>
// ----- Demo styles ----- //

.demo.g-block {
  margin: 0 auto;
  min-height: auto;
}

.about {
  color: var(--theme-text_1);
  font-size: 1.4rem;
}

.area {
  display: flex;
  justify-content: space-between;
  align-items: center;

  &End {
    position: relative;
    text-align: right;
  }
}

.fieldLabel {
  font-size: 1.8rem;
  font-weight: 600;
  margin-right: 4px;
}

.fieldInput {
  width: 50px;
  height: 40px;
  border: none;
  border: 1px solid var(--theme-text_1);
  border-radius: 4px;
  text-align: center;
  font-size: 1.8rem;

  &:focus {
    outline: none;
    box-shadow: var(--focus-shadow);
  }

  /* Chrome, Safari, Edge, Opera */
  &::-webkit-outer-spin-button,
  &::-webkit-inner-spin-button {
    -webkit-appearance: none;
    margin: 0;
  }

  /* Firefox */
  &[type="number"] {
    -moz-appearance: textfield;
  }
}

.btnSubmit {
  --btnTxt: #fff;
  position: relative;
  display: inline-block;
  cursor: pointer;
  min-height: 44px;
  padding: 2px 20px;
  background-color: var(--theme-primary);
  border-radius: 4px;
  border: none;
  font-size: 1.8rem;
  color: var(--btnTxt);
  text-align: center;
  transition: color 250ms;

  /* [DIFF 1/2] - change selector for styles */
  &:hover:not([aria-disabled="true"]) {
    opacity: 0.8;
  }

  &:focus:not(:focus-visible) {
    outline: none;
  }

  &:focus-visible {
    outline: none;
    box-shadow: var(--focus-shadow);
  }

  /* [DIFF 2/2] - change selector for styles */
  &[aria-disabled="true"] {
    opacity: 0.7;
    cursor: not-allowed;
  }

  // loading indicator
  &::after {
    content: "";
    position: absolute;
    display: block;
    width: 0.7em;
    height: 0.7em;
    top: calc(50% - 0.5em);
    left: calc(50% - 0.5em);
    border: 2px var(--btnTxt);
    border-bottom-color: transparent;
    border-left-color: transparent;
    border-style: solid;
    border-radius: 50%;
    opacity: 0;
    transition: opacity 250ms;
  }

  &[data-loading="true"] {
    color: transparent;
    pointer-events: none;

    &::after {
      opacity: 1;
      animation: rotate 750ms linear infinite;
    }

    .btnSubmit-text {
      visibility: hidden;
    }
  }
}

.formStatus {
  position: absolute;
  top: 100%;
  right: 0;
  font-size: 1.3rem;
  color: green;
  margin-top: 6px;
  white-space: nowrap;
}

.terms {
  position: relative;
  margin: 16px 0 0;
  text-align: center;
}

.tooltipBox {
  position: absolute;
  width: 104px;
  bottom: 100%;
  left: calc(50% - 52px);
  padding-bottom: 4px; /* use padding to preserve hover when moving cursor between the tooltip button and the tooltipItself */

  opacity: 0;
  visibility: hidden;

  /* delay 250ms to give time to fade out */
  transition: opacity 250ms, visibility 1ms 250ms;

  .tooltipArea.isActive:hover &,
  .tooltipArea.isActive:focus-within & {
    opacity: 1;
    visibility: visible;
    transition: opacity 250ms;
  }

  .tooltipArea.isActive:hover & {
    /* delay fadein 500ms to prevent accidental hovers */
    transition: opacity 250ms 500ms;
  }
}

.tooltipItself {
  display: block;
  background: hsl(266deg 100% 15%);
  color: hsl(266deg 100% 96%);
  padding: 6px 8px;
  font-size: 1.3rem;
  border-radius: 4px;
  text-align: center;
  -webkit-font-smoothing: initial;
  -moz-osx-font-smoothing: initial;
}

@keyframes rotate {
  0% {
    transform: rotate(0deg);
  }
  100% {
    transform: rotate(360deg);
  }
}

// ----- Theme styles ----- //

html {
  font-size: 62.5%;
  --theme-width: 400px;
  --theme-text_0: hsl(0deg 0% 20%);
  --theme-text_1: hsl(0deg 0% 43%);
  --theme-bg_0: hsl(27deg 39% 95%);
  --theme-bg_1: hsl(0deg 0% 100%);
  --theme-primary: hsl(266deg 100% 61%);
  --theme-primary_smooth: hsl(266deg 100% 92%);
  --theme-secondary: hsl(27deg 100% 56%);

  --focus-shadow: var(--theme-bg_0) 0 0 0 2px, var(--theme-secondary) 0 0 0 4px;
}

body {
  background-color: var(--theme-bg_0);
  color: var(--theme-text_0);
  font-family: "IBM Plex Sans", sans-serif;
  font-size: 1.6rem;
  font-weight: 300;
  box-sizing: border-box;
  color: #343434;
  line-height: 1.5;
  padding: 45px 16px 0;

  @media screen and (min-width: 40em) and (min-height: 27em) {
    padding-bottom: 90px; /* for fixed footer */
  }
}

* {
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

body * {
  box-sizing: inherit;
}

p {
  margin: 0;
}

.g-hero {
  text-align: center;
  margin-bottom: 32px;

  h1 {
    font-size: 2.1rem;
    margin: 0;
  }
}

// Global components / Utils
.g-block {
  position: relative;
  max-width: var(--theme-width);
  padding: 32px 16px;
  margin: 24px auto 60px;
  border: 1px solid var(--theme-primary);
  border-radius: 4px;
  background-color: var(--theme-bg_1);
  box-shadow: 2px 2px var(--theme-primary_smooth);
  min-height: 100px;
}

.g-blockTitle {
  position: absolute;
  top: -30px;
  left: 0;
  text-transform: uppercase;
  font-size: 1.6rem;
  margin: 0;
  font-weight: 600;
}

.g-footer {
  position: relative;
  width: 100%;
  margin-top: 60px;
  padding: 24px 16px;
  text-align: center;
  font-size: 1.4rem;
  background: var(--theme-bg_1);

  @media screen and (min-height: 26em) {
    position: fixed;
    left: 0;
    bottom: 0;
  }
}

.u-link {
  --linkClr: var(--theme-primary);
  position: relative;
  text-decoration: underline;
  text-decoration-color: var(--linkClr);
  color: inherit;
  z-index: 0;
  white-space: nowrap;

  &:hover::before,
  &:focus::before {
    transform: scale(1, 1);
  }

  &:focus {
    outline: none;
    border-radius: 4px;
    box-shadow: var(--focus-shadow);
  }

  &::before {
    content: "";
    position: absolute;
    bottom: 0.05em;
    left: -0.1em;
    width: calc(100% + 0.2em);
    height: 1.2em;
    background-color: var(--linkClr);
    border-radius: 3px;
    opacity: 0.2;
    transform: scale(1, 0.2);
    transform-origin: 0 95%;
    z-index: -1;
    transition: transform 175ms ease-out;
  }
}

.u-link:hover {
  outline: none;
}

/* screen reader only */
.sr-only {
  position: absolute;
  width: 1px;
  height: 1px;
  padding: 0;
  margin: -1px;
  overflow: hidden;
  clip: rect(0, 0, 0, 0);
  border: 0;
}
View Compiled
const elForm = document.querySelector(".js-form");
const elBtnSubmit = document.querySelector(".js-btnSubmit");
const elTickets = document.querySelector(".js-tickets");
const elFeedback = document.querySelector(".js-feedback");
const elTooltip = document.querySelector(".js-tooltip");

let ticketsCount = 0;
let isSubmitting = false;

const checkTicketsValidation = () => ticketsCount >= 1 && ticketsCount <= 9;

elBtnSubmit.addEventListener("click", handleBtnClick);

elTickets.addEventListener("keyup", handleCountChange);
elTickets.addEventListener("change", handleCountChange);

elForm.addEventListener("submit", handleFormSubmit);

function handleCountChange(event) {
  ticketsCount = Number(event.target.value);
  const hasValidTickets = checkTicketsValidation();

  if (hasValidTickets) {
    /* [DIFF] 1/2 - change attribute from disabled to aria-disabled */
    elBtnSubmit.setAttribute("aria-disabled", "false");
    elTooltip.classList.remove("isActive");
  } else {
    elBtnSubmit.setAttribute("aria-disabled", "true");
    elTooltip.classList.add("isActive");
  }

  elFeedback.innerText = "";
}

function handleBtnClick(event) {
  // Do nothing. the submit happens on the form submit
}

async function handleFormSubmit(event) {
  event.preventDefault(); // avoid native form submit (page refresh)

  /* [DIFF] 2/2 - verify if button has aria-disabled="true" */
  const isBtnDisabled = elBtnSubmit.getAttribute("aria-disabled") === "true";

  if (isBtnDisabled) {
    console.log("Disabled submit prevented");
    return;
  }

  if (isSubmitting) {
    console.log("Double submit prevented");
    return;
  }

  isSubmitting = true;

  elFeedback.innerText = "";
  elBtnSubmit.setAttribute("data-loading", "true");
  // Explicit set the button loading action for screen readers
  const elLoadingStatus = elBtnSubmit.querySelector(".js-loadingMsg");
  elLoadingStatus.innerText = elLoadingStatus.getAttribute("data-loading-msg");

  await fakeWaitTime(1500);

  elLoadingStatus.innerText = "";
  elFeedback.innerText = `Added ${ticketsCount} tickets!`;
  elBtnSubmit.setAttribute("data-loading", "false");

  isSubmitting = false;
}

function fakeWaitTime(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

This Pen doesn't use any external JavaScript resources.