Bootstrap 5 has a on/off toggle buttons out of the box through the button plugin.

But you aren't always using the same frameworks. In my case, I was using Tailwind.

Front-end frameworks allow you to add state to your page that could be used to create this same functionality.

But bringing in an entire framework to create a toggle button feels like using a sledgehammer to crack a nut.

A sasquatch surrounded by bees, smashing a nut with a sledgehammer in his kitchen.
Can we just take a moment to appreciate how stupid this AI generated photo is? Why do these bees keep showing up unprompted?

All right, well let's just do it ourself then.

I always like to start out by writing HTML in the way I'd like to write it.

<button type="button" data-toggle-button>Toggle Button</button>

We use the type="button" to prevent form submissions.

The data-toggle-button attribute will be our magic.

But it won't work without JavaScript, so let's start by grabbing every element with that attribute.

We'll also throw a role="button" attribute on there for accessibility. This isn't necessary if you put data-toggle-button on an actual button element, but this allows us to put it on other elements too.

document.querySelectorAll("[data-toggle-button]").forEach((element) => {
  element.setAttribute("role", "button");
});

Next, we need a way to track our button's state.

We can do that in an accessible way with the ARIA attribute aria-pressed.

And we need to change that attribute when our button is clicked.

So, add a click event listener to all these buttons, and set the value of aria-pressed to the opposite of its current state.

We won't be able to see it visually, but our state is changing on each click. I'm listing the element's attributes below the button so you can see it change.

document.querySelectorAll("[data-toggle-button]").forEach((element) => {
  element.setAttribute("role", "button");
  element.addEventListener("click", () => {
    let pressed = element.getAttribute("aria-pressed") === "true";
    element.setAttribute("aria-pressed", !pressed);
  });
});
Button Attributes

    Okay, so now that we have state, we can use it to style our button.

    Here is a minimal example. Modify this to your liking, and put it in your stylesheet.

    [data-toggle-button] {
        border: 0;
        background: #ee6055;
        color: #fff;
    }
    [data-toggle-button][aria-pressed=true] {
        background: #60d394;
        color: #fff;
    }
    

    Now we have a working toggle button.

    But it's off when you load the page. What if you want it on when the page loads?

    Let's add the ability to set the value of the toggle button.

    Again, let's write the HTML first.

    <button data-toggle-button data-toggle-value="true" type="button">A button that's already pressed</button>
    

    We now have data-toggle-value attribute which we assign a boolean.

    And the button says it's already been pressed, but it hasn't. So let's modify our JavaScript to do that.

    We can access data attributes with the dataset property on any element.

    Our attributes kebab casing in HTML is automatically converted to camel casing in the dataset object.

    First, we check if the toggle value is defined or "false". Second, we set toggleValue and aria-pressed to that false if it is.

    Third, in our pressed variable, we need to update our toggle value to match the aria-pressed value.

    document.querySelectorAll("[data-toggle-button]").forEach((element) => {
      element.setAttribute("role", "button");
    
      // 1. Check if the toggle value is not present or false.
      if (element.dataset.toggleValue === undefined || element.dataset.toggleValue === "false") {
          // 2. Set the aria-pressed and toggle value to false.
          element.setAttribute("aria-pressed", "false");
          element.dataset.toggleValue = "false";
      } else if (element.dataset.toggleValue === "true") {
          // 3. Update the aria-pressed to match toggleValue.
          element.setAttribute("aria-pressed", "true");
      }
      
      element.addEventListener("click", () => {
        // 3. Check for aria-pressed or a toggle value.
        let pressed = element.getAttribute("aria-pressed") === "true" || element.dataset.toggleValue === "true";
        element.setAttribute("aria-pressed", !pressed);
        // 4. Update our toggle value as well.
        element.dataset.toggleValue = !pressed;
      });
    })
    

    Boom! Easy peasy. We now have a reusable script that we can put anywhere to create accessible toggle buttons.

    We can get really creative with our styling to do really cool things.

    One example use case: The menus after logging in to this site are powered by this JavaScript we just wrote.

    In combination with these styles.

    .sub-menu-toggle + .sub-menu {
      display: none;
    }
    
    .sub-menu-toggle[aria-pressed=true] + .sub-menu {
      display: block !important;
    }
    

    That's it! I'll be writing more developer blog posts like this in the future!

    If you liked it, follow us on Twitter/X so you'll see the next one!

    Here are the final code snippets cleaned up if you'd like to use them in your own web development projects.

    <button type="button" data-toggle-button data-toggle-value="true">Toggle Button</button>
    
    document.querySelectorAll("[data-toggle-button]").forEach((element) => {
      element.setAttribute("role", "button");
      if (element.dataset.toggleValue === undefined || element.dataset.toggleValue === "false") {
          element.setAttribute("aria-pressed", "false");
          element.dataset.toggleValue = "false";
      } else if (element.dataset.toggleValue === "true") {
          element.setAttribute("aria-pressed", "true");
      }
      element.addEventListener("click", () => {
        let pressed = element.getAttribute("aria-pressed") === "true" || element.dataset.toggleValue === "true";
        element.setAttribute("aria-pressed", !pressed);
        element.dataset.toggleValue = !pressed;
      });
    })
    
    Nick Adams - Founder of Arcane Web Design
    Nick Adams | Founder of Arcane Web Design

    With a decade of web design and development experience, Nick Adams has a proven track record of creating impactful digital solutions for businesses across various sectors. He's honed his skills in web design, front-end development, back-end development, web accessibility, and has helped companies improve foot traffic through improved search engine rankings. He's won several awards in graphic and web design.