Published on

Web Accessibility 101: A Step-by-Step Guide to Building an Accessible Quiz

Authors

'Web Accessibility 101: A Step-by-Step Guide to Building an Accessible Quiz'

Learn the fundamentals of web accessibility (a11y) by building a fully functional and accessible quiz from scratch using semantic HTML, ARIA, and JavaScript. A practical guide for developers.

Table of Contents

Ever built a cool, interactive quiz for your website, portfolio, or learning platform? They're fantastic for engagement. But have you ever stopped to think if everyone can use it? What about someone who navigates with a keyboard, or someone who uses a screen reader to interpret the web?

This is where web accessibility (often abbreviated as a11y) comes in. It's not just a 'nice-to-have' or a box to tick; it's a fundamental practice of inclusive design that ensures your creations are usable by people with a wide range of disabilities. And interactive components like quizzes are a prime area where accessibility can often break down.

In this comprehensive guide, we're going to roll up our sleeves and build a simple, multi-question quiz from the ground up, focusing on accessibility at every step. You'll learn not just what to do, but why you're doing it. By the end, you'll have a solid foundation for making all your future interactive components more inclusive.

Ready? Let's get building.

The Anatomy of Our Accessible Quiz

Before we write a single line of code, let's define what we're building. Our quiz will have:

  • Multiple questions.
  • Multiple-choice answers for each question (using radio buttons).
  • A way to check the answer for each question.
  • Immediate feedback (e.g., "Correct!" or "Incorrect.").
  • A final score at the end.

Our primary tools will be semantic HTML, a dash of CSS for clarity, and some vanilla JavaScript to handle the logic. The principles here are universal and can be applied to any framework like React, Vue, or Svelte.

Section 1: The Bedrock - Semantic HTML

If you take away only one thing from this article, let it be this: Start with semantic HTML. Before you reach for fancy JavaScript libraries or complex ARIA attributes, a well-structured HTML document provides a massive head start for accessibility. Browsers and assistive technologies (like screen readers) have built-in understanding of semantic tags.

For a quiz, the most semantic parent container is a <form>. A quiz is, after all, a form of user input.

Grouping Questions with <fieldset> and <legend>

How do you group a question with its set of possible answers? The perfect tool for this job is the <fieldset> element. It tells assistive technologies that everything inside it belongs to a single group. And to label that group, we use the <legend> element.

When a screen reader user navigates to the first radio button inside a <fieldset>, it will announce the <legend> first, giving crucial context. For example: "Question: What is the capital of France? group. Paris, radio button, 1 of 4."

Without <fieldset> and <legend>, the user would just hear "Paris, radio button, 1 of 4." Paris what? The context is lost.

Let's structure our first question:

<form class="quiz-form">
  <fieldset class="question-group">
    <legend class="question-text">1. What is the powerhouse of the cell?</legend>
    
    <div class="answers-container">
      <div class="answer">
        <input type="radio" name="q1" value="nucleus" id="q1-a1">
        <label for="q1-a1">Nucleus</label>
      </div>
      <div class="answer">
        <input type="radio" name="q1" value="ribosome" id="q1-a2">
        <label for="q1-a2">Ribosome</label>
      </div>
      <div class="answer">
        <input type="radio" name="q1" value="mitochondria" id="q1-a3">
        <label for="q1-a3">Mitochondria</label>
      </div>
    </div>
  </fieldset>

  <!-- More questions will go here -->
</form>

Let's break down the key parts:

  • <fieldset>: Groups the question and its answers.
  • <legend>: Provides the question text. It's the only reliable way to label a fieldset.
  • <input type="radio">: The standard for single-choice selections.
  • name="q1": All radio buttons for the same question must have the same name attribute. This is what ensures a user can only select one.
  • <label for="..."> and id="...": This is a non-negotiable pairing. The for attribute of the label must match the id of the input. This programmatically links them, which means a user can click on the label to select the radio button, and screen readers will correctly announce the label when the input is focused.

Section 2: Keyboard Navigation and Focus Management

People with motor disabilities, as well as many power users, rely on a keyboard to navigate the web. Fortunately, by using standard form elements, we get a lot of this for free!

  • Tab: Users can press the Tab key to move focus from one interactive element to the next (in our case, from one radio button group to the next).
  • Arrow Keys: Once a radio button in a group is focused, users can use the up/down or left/right arrow keys to cycle through the options in that group. This is default browser behavior for radio inputs with the same name.
  • Spacebar: Selects the focused radio button.

Don't Remove the Outline!

One of the biggest accessibility mistakes is removing the focus outline. You've probably seen this CSS snippet:

/* Please don't do this! */
:focus {
  outline: none;
}

This makes it impossible for keyboard users to see where they are on the page. If you must create a custom focus style, do it accessibly. A modern approach is to use the :focus-visible pseudo-class, which typically applies focus styles only for keyboard users, not mouse clicks.

Here's a better way:

/* A clear, high-contrast focus style */
:focus-visible {
  outline: 3px solid #005fcc; /* A nice blue outline */
  outline-offset: 2px;
  border-radius: 4px; /* Optional: matches element's border-radius */
}

/* For our radio buttons inside the quiz */
.quiz-form input[type="radio"]:focus-visible + label {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
  border-radius: 4px;
}

/* A fallback for older browsers that don't support :focus-visible */
.quiz-form input[type="radio"]:focus + label {
  outline: 3px solid #005fcc;
  outline-offset: 2px;
  border-radius: 4px;
}

By styling the label when the input is focused, we create a larger, more visible focus indicator that wraps around the text, which is much clearer than a tiny ring around the radio button itself.

Section 3: Announcing Feedback with ARIA Live Regions

This is where things get really interesting. Imagine a sighted user selects an answer. We can show them a green "Correct!" message. But how does a screen reader user know this? They can't see the color change or the new text that appeared on the screen.

We need to tell the screen reader to announce the change. The tool for this is an ARIA live region.

An ARIA live region is an element that you designate as 'live'. When its content changes, screen readers will automatically announce the change without the user having to move their focus.

There are two main politeness settings:

  1. aria-live="polite": The screen reader will wait for a moment of silence (e.g., when the user stops talking or navigating) before announcing the change. This is what you want 99% of the time. It's not disruptive.
  2. aria-live="assertive": The screen reader will interrupt whatever it's doing and announce the change immediately. Use this very sparingly, for critical, time-sensitive errors (e.g., "Session timeout in one minute.").

Let's add a feedback area to our question HTML and a button to check the answer.

<!-- Inside the fieldset, after the answers -->
<div class="question-feedback" aria-live="polite"></div>
<button type="button" class="check-answer-btn">Check Answer</button>

Now for the JavaScript. We'll add an event listener to our button. When clicked, it will check the selected answer and update the content of our aria-live region.

// This is a simplified example. In a real app, you'd loop through questions.
document.addEventListener('DOMContentLoaded', () => {
  const quizForm = document.querySelector('.quiz-form');
  const questionGroup = quizForm.querySelector('.question-group');
  const checkBtn = questionGroup.querySelector('.check-answer-btn');
  const feedbackEl = questionGroup.querySelector('.question-feedback');

  // Let's say this is our correct answer data
  const correctAnswers = {
    q1: 'mitochondria'
  };

  checkBtn.addEventListener('click', () => {
    const selectedRadio = questionGroup.querySelector('input[name="q1"]:checked');

    if (!selectedRadio) {
      feedbackEl.textContent = 'Please select an answer first.';
      return;
    }

    const userAnswer = selectedRadio.value;
    if (userAnswer === correctAnswers.q1) {
      feedbackEl.textContent = 'Correct! The mitochondria is the powerhouse of the cell.';
      feedbackEl.classList.add('correct');
      feedbackEl.classList.remove('incorrect');
    } else {
      feedbackEl.textContent = `Incorrect. The correct answer is Mitochondria.`;
      feedbackEl.classList.add('incorrect');
      feedbackEl.classList.remove('correct');
    }
  });
});

Now, when a user clicks the button, a sighted user sees the message, and a screen reader user hears, for example: "Correct! The mitochondria is the powerhouse of the cell." This is a huge win for accessibility!

Don't Rely on Color Alone

Notice in our JavaScript we also added CSS classes (correct, incorrect). You might be tempted to just do this:

.correct { color: green; }
.incorrect { color: red; }

This fails WCAG Success Criterion 1.4.1: Use of Color. People with color blindness might not be able to distinguish between the red and green. Always ensure that information conveyed by color is also available through other means.

Our text content ("Correct!" vs. "Incorrect.") already does this, but you could enhance it further with icons:

.correct::before {
  content: '✔ ';
  color: green;
}

.incorrect::before {
  content: '✖ ';
  color: red;
}

This combination of text, symbols, and color provides a robust, multi-sensory user experience.

Section 4: The Grand Finale - Calculating and Presenting Results

Once the user has answered all the questions, they need to submit the quiz and see their final score.

First, let's add a submit button to our form and a container for the final results.

<!-- After all the fieldsets in the form -->
<button type="submit" class="submit-quiz-btn">Get My Score</button>

<!-- After the form -->
<div class="quiz-results" id="quiz-results-container" tabindex="-1">
  <h2>Your Results</h2>
  <p id="score-text"></p>
</div>

Notice the tabindex="-1" on the results container. This is important. By default, you can only focus() on interactive elements like links and inputs. tabindex="-1" allows an element to be focused programmatically via JavaScript, but it doesn't add it to the normal Tab order. This is exactly what we need.

Now, let's wire up the submission logic.

// Continuing our previous script
const quizForm = document.querySelector('.quiz-form');
const resultsContainer = document.querySelector('#quiz-results-container');
const scoreText = document.querySelector('#score-text');

// Let's imagine we have more questions now
const correctAnswers = {
  q1: 'mitochondria', 
  q2: 'paris'
};

quizForm.addEventListener('submit', (event) => {
  event.preventDefault(); // Stop the form from actually submitting to a server

  let score = 0;
  const totalQuestions = Object.keys(correctAnswers).length;
  const allQuestions = quizForm.querySelectorAll('.question-group');

  allQuestions.forEach((question, index) => {
    const questionName = `q${index + 1}`;
    const selectedRadio = question.querySelector(`input[name="${questionName}"]:checked`);
    if (selectedRadio && selectedRadio.value === correctAnswers[questionName]) {
      score++;
    }
  });

  // Display the results
  scoreText.textContent = `You scored ${score} out of ${totalQuestions}!`;
  resultsContainer.style.display = 'block'; // Make it visible

  // The magic! Move focus to the results heading.
  resultsContainer.focus();
});

When the form is submitted:

  1. We prevent the default page reload.
  2. We loop through the questions and calculate the score.
  3. We update the text in our results container.
  4. Crucially, we call resultsContainer.focus().

This last step is vital for screen reader users. After they click the "Get My Score" button, their focus is programmatically moved to the results container. The screen reader will then announce "Your Results, heading level 2. You scored 1 out of 2!". Without this, the user would be left stranded on the submit button, unaware that their score has appeared elsewhere on the page.

Section 5: Beyond the Basics - Further Considerations

We've built a solid, accessible foundation. Here are a few more things to keep in mind for more complex quizzes.

  • Progress Indicators: For long quizzes, it's helpful to tell users where they are. You can include text like Question 1 of 5 in your <legend>. For screen reader users, you could also add a visually hidden <span> that gets updated, placed inside an aria-live region, to announce progress as they move between questions.

  • Time Limits: If your quiz has a time limit, this must be handled carefully. According to WCAG 2.1 (2.2.1 Timing Adjustable), you must give users the ability to turn off, adjust, or extend the time limit, unless it's essential (like in a live auction). If you have a countdown timer, its updates should be announced periodically using an aria-live region.

  • Clear Instructions: Don't assume users know how to interact with your quiz. Add a short introductory paragraph before the quiz begins, explaining how it works. e.g., "Use the Tab key to move between questions and the arrow keys to select an answer. Your progress will be saved as you go."

Conclusion: Building an Inclusive Web, One Quiz at a Time

We've journeyed from a blank slate to a functional, accessible quiz. Let's recap the core principles we've applied:

  1. Semantic HTML is King: We used <form>, <fieldset>, <legend>, and <label> to give our quiz a meaningful structure that assistive technologies understand out of the box.
  2. Ensure Keyboard Navigability: By using standard inputs and preserving focus outlines (:focus-visible), we made sure keyboard-only users can operate the quiz with ease.
  3. Provide Non-Visual Feedback: We used aria-live="polite" to announce dynamic changes (like 'Correct'/'Incorrect' feedback) to screen reader users, ensuring they have the same information as sighted users.
  4. Manage Focus Intelligently: After a major action like submitting the quiz, we programmatically moved focus to the results area, creating a seamless experience.
  5. Don't Rely on Color Alone: We ensured that all information conveyed with color was also available through text and symbols.

The best way to truly understand accessibility is to test it yourself. Try navigating your own quiz using only the keyboard. Download a free screen reader like NVDA (for Windows) or use the built-in VoiceOver (macOS) or TalkBack (Android) and experience your site through the ears of another user. You'll be amazed at what you learn.

Building accessible components isn't an afterthought; it's a hallmark of high-quality front-end development. By embedding these practices into your workflow, you're not just complying with standards—you're building a more welcoming and equitable web for everyone.