Lab 10: Accessibility

In this lab, we will:

  • Learn how to make visualizations accessible to screen readers and keyboard users
  • Improve interactivity and focus styles for SVG elements
  • Use ARIA roles, labels, and properties effectively
  • Practice adding descriptive text and summaries to support diverse users
Table of contents

Check-off

You need to come to TA Office Hours to get checked off for this lab (any of them, no appointment needed), OR submit your work asynchronously by filling out this form.

If you choose to submit your work asynchronously and have an incorrect or incomplete part of the lab, you will not receive any credit for the lab (we do not offer partial credit on labs). You may not resubmit this form nor ask for a synchronous check off for the same lab.

Lab 10 Rubric

To successfully complete this lab check-off, ensure your work meets all of the following requirements:

General

  • Your implementation of this lab is a project accessible from your portfolio webpage.
  • You tested your visualizations using a screen reader (VoiceOver or NVDA) and made necessary fixes.

Pie Chart

  • Pie slices are keyboard focusable
  • Pie slices use appropriate ARIA attributes
  • Your <svg> element includes a descriptive <title> and <desc>
  • You implemented live region to announce the currently selected pie slice
  • Your pie chart uses an accessible color palette and includes focus-visible styling.
  • Your pie chart includes a tabular representation
  • Your projects page includes a button to toggle between the pie chart and table

Scatterplot (Optional)

  • Each datapoint is keyboard focusable
  • Each dot includes an accessible label
  • The tooltip uses role = "tooltip" and is activated on hover and focus.

Search Bar

  • The 🔍 emoji has been moved out of the text placeholder
  • The emoji is hidden from screenreaders using aria-hidden = "true"

Slides

Please find the slides here.

What will we make?

In this lab, we will be building on the interactive visualizations you created in Labs 6 and 7, and transforming them into visually and non-visually accessible experiences. You’ll:

  • Fix common accessibility issues flagged by Svelte (like focusable SVG elements)
  • Make charts navigable by keyboard users
  • Add screen reader-friendly titles, descriptions, and live region updates
  • Improve visual accessibility by choosing color-blind accessible color palettes

By the end of this lab, your pie chart and scatterplot will be easier to use for everyone — regardless of how they interact with the web.

Step 0: Complete Lab 6 and 7 if you haven’t already done so

In this lab we will be building off of your work in Lab 6 and Lab 7. If you have not completed them, you should do that now.

Step 0.1: Install and Try a Screen Reader

The first step is to experience how blind or low-vision users interact with web content using a screen reader. This will help you understand the importance of making your visualizations and websites accessible.

Windows users can either use:

  • Narrator: built-in and easy to start
  • NVDA: a free, open-source screen reader used widely in the accessibility community

Mac users can use:

To turn on Narrator (Windows):

  1. Go to the bottom-right of your taskbar
  2. Click on Accessibility Settings
  3. Toggle Narrator to On

To turn on VoiceOver(Mac) you can follow these steps:

  1. Apple menu
  2. System Settings
  3. then click Accessibility in the sidebar (you may need to scroll down).
  4. Click VoiceOver, then turn VoiceOver on or off

(And don’t forget to turn up your audio!)

Screen readers translate the contents of a page into speech output. They rely on semantic HTML and ARIA labels to understand page structure. You can navigate web pages using:

  • Tab: jump between interactive elements (like buttons, links, form fields)
  • Arrow keys: move through content line-by-line or item-by-item
  • H / Shift + H: navigate by heading (e.g., in VoiceOver and NVDA)
  • Insert + F7 (NVDA): open an element list (headings, links, landmarks)
  • Control + Option + U (VoiceOver): open the rotor to explore page structure

Please feel free to refer to this Voice Over Cheat Sheet for Mac users and this Narrator Cheat Sheet for Windows users.

Most screen readers have keyboard shortcuts to quickly jump to:

  • Headings
  • Landmarks (like navigation or main content)
  • Links
  • Form controls

Screen readers announce what kind of element you’re on (e.g., “Heading level 1: Accessibility Lab”), and whether it’s clickable, selected, or editable. This makes semantic structure critical—using proper headings (<h1>, <h2>), labels (aria-label, aria-labelledby), and focusable elements is key for accessibility.

Here is a sneak peak of launching narrator on windows, and a preview of the site after making accessibility changes:

Step 1: Making Elements Keyboard Focusable

Step 1.1: Making the Pie Chart Keyboard Focusable

Remember the pie chart you made in Lab 6 Step 5 to filter your projects by year - it should looks something like this:

As you may remember, in our src/lib/Pie.svelte we had a yellow squiggly line indicating an Accessibility warning. These built-in warnings are great reminders from Svelte to think about how accessible our applications are when we make them. In this section of the lab we will learn to address warnings like these.

It’s important to understand why these warnings are there. The path elements are not focusable by default, so they cannot be interacted with using the keyboard. This means that as it currently stands, people who cannot use a mouse or other pointing device cannot select a wedge. Even users who can use a mouse, often find keyboard interactions more convenient (e.g. imagine filling out a form by clicking on each field with the mouse instead of pressing Tab!).

So how do we fix this? The first step is making it possible to interact with these wedges with the keyboard at all. Right now, you cannot even select a wedge by pressing the Tab key on your keyboard, because they are not focusable.

We can fix this by adding a few attributes to the <path> elements:

We’re not done yet. All that these do is to make sure users of assistive technology can actually interact with the wedge. However, because it’s not a native button or link, the click event will not be triggered when the user focuses on the wedge with the keyboard and presses Enter or Space. Instead, we need to enable that, via a separate event listener (keyup is a good candidate).

To avoid duplicating code, let’s move the code that selects a wedge into a separate function:

function toggleWedge (index, event) {
	selectedIndex = index;
}

Then replace on:click={e => selectedIndex = index} with on:click={e => toggleWedge(index, e)}. Now add a keyboard event listener: on:keyup={e => toggleWedge(index, e)}.

In the toggleWedge function, we can wrap the code that selects the wedge with a conditional that checks that either event.key doesn’t exist, or if it does, that it is Enter:

function toggleWedge (index, event) {
	if (!event.key || event.key === "Enter") {
		selectedIndex = index;
	}
}

If you try the keyboard interaction (first try reaching the pie chart with your keyboard arrows and then pressing Tab + Enter to go through each wedge) you will notice that it works, but even when we are interacting with it via the mouse, we get an unwieldy focus ring around the wedge which looks awful since it’s actually covered by the other wedges:

We can hide that with outline: none:

path {
	transition: 300ms;
	outline: none;
}

However, now keyboard users have no way to know which wedge they have currently focused, which is a terrible user experience. Never, ever remove the browser’s default focus styles without providing alternative focus styles. Often extending :hover styles to cover :focus-visible as well is a good start. So let’s extend our previous :hover effect to keyboard users as well:

svg:hover path:not(:hover), svg:focus-visible path:not(:focus-visible) { opacity: 50%; }

If you try out the keyboard interaction now, you will notice that we are getting a visible indication of focus, and that the unwieldy default focus ring is no longer visible. Yay! 🎉

As you start applying ARIA attributes like aria-label, aria-labelledby, and aria-describedby, you might find yourself wondering—what’s the difference between them, and when should you use each?

These attributes help screen readers understand and communicate what elements are, how they relate to other elements, and any additional descriptions users should hear. But the names and use cases can be a bit confusing at first.

To help clarify, here’s a quick reference guide to when and why you’d use each one:

Attribute What it does When to use it
aria-label Gives the element an explicit label. Use when there’s no visible label (e.g. icon-only buttons or SVG elements).
aria-labelledby Uses the ID of another element to label this element. Use when there’s a visible label (e.g. heading or label nearby).
aria-describedby Provides a longer description (via an element ID). Use when you want to offer supplementary info, like tooltips or hints.

Think of aria-label as “what is this?”, aria-labelledby as “what text on the page already explains this?”, and aria-describedby as “what extra info helps explain it?”

Step 1.2: Making the Scatterplot Keyboard Focusable (Optional)

Similarly to how we fixed the accessibility of the pie chart, now, let’s fix the accessibility of the scatterplot we made in Lab 7. As you may recall, when we added information in step 3.1 to the tooltip, we also had a yellow squiggly line indicating an Accesibility warning. Take a look at your file src/routes/meta/+page.svelte - you should see something like this:

To fix the accessibility issues, we should also add:

  • tabindex="0" to the dots to make them focusable
  • aria-describedby="commit-tooltip" to the dots to link them to the tooltip for assistive technology
  • role="tooltip" to the tooltip to indicate its purpose to assistive technology
  • aria-haspopup="true" to the dots to indicate that they have a tooltip
  • on:focus and on:blur event listeners (that also call dotInteraction())

Step 2: Writing Alt Text for Images and Visualizations

Screen readers can’t automatically interpret the meaning of visual elements like SVG charts or images. Without additional context, users who rely on assistive technologies may miss key information. In this section, we’ll learn how to make our pie chart accessible by adding meaningful descriptions that screen readers can detect and announce.

Writing good alt text helps convey what the image is and why it matters. For data visualizations, this means focusing on the key trends, outliers, or takeaways.

Here are some quick rules of thumb:

  • ✅ Summarize the takeaway, not every bar or point (unless that’s the point).
  • ✅ Use clear and concise language.
  • 🚫 Avoid phrases like “Image of…” or “Graph of…” — screen readers already announce the element type.
  • 🚫 Don’t just repeat the caption — expand it.

Examples:

  • ❌ “Bar chart of housing prices.”
  • ✅ “Bar chart showing housing prices have doubled from 2020 to 2023.”

  • ❌ “Line graph showing temperatures.”
  • ✅ “Line graph showing steady temperature rise over the last decade, peaking in 2023.”

Here are some additional resources on writing alt text from the Web Accessibility Initiative and this Medium Article on Mastering Alt Text

Step 2.1: Adding Static Alt Text to the Pie Chart

Try turning on your screen reader — NVDA on Windows or VoiceOver on macOS (don’t forget to turn up your audio!). Navigate to your local site and hover or tab to your project pie chart.

👉 You’ll notice something surprising: the chart is completely silent. Screen readers skip over it as if it doesn’t exist.

Let’s fix that.

Open src/lib/Pie.svelte, and locate your <svg> element. It should look like this:

<svg viewBox="-50 -50 100 100">
  <!-- your code for the paths -->
</svg>

We’ll make it accessible by adding:

  • role="img" – tells screen readers this is an informative visual.
  • <title> and <desc> – provide a name and summary for the chart.
  • aria-labelledby – links the <title> and <desc> to the SVG so screen readers read them aloud.

Here’s what your updated <svg> should look like:

<svg 
  viewBox="-50 -50 100 100" 
  role="img" 
  aria-labelledby="pie-title pie-desc">
  <title id="pie-title">Projects by Year</title>
  <desc id="pie-desc">A pie chart showing the number of projects per year. Most projects are in 2024 and 2025.</desc>
  <!-- your code for the paths -->
</svg>

Now try testing with a screen reader again. You should hear the title and description of the chart read out loud. 🎉

Step 2.2: Generating Dynamic Alt Descriptions for the Pie Chart

Our current alt text is hardcoded — but what happens if the data changes? For example, if you add more projects in 2026, the description will no longer be accurate.

Let’s make the chart description dynamic, so it always reflects the current dataset.

In the <script> section of src/lib/Pie.svelte, add a computed variable like this:

$: description = `A pie chart showing project counts by year. ${data.map(d => `${d.label}: ${d.value} projects`).join(', ')}.`;

and after the svg but before each path, add this line:

<desc id="pie-desc">{description}</desc>

Now your screen reader will say something like:

“A pie chart showing project counts by year. 2025: 4 projects, 2024: 8 projects.”

This way, the description always stays in sync with the underlying data — no need to update it manually!

Step 2.3: Adding Live Region Updates to the Pie Chart

When a user selects a slice of the pie (e.g. using arrow keys or the mouse), we want screen reader users to hear which year and how many projects were selected. We can achieve this using an ARIA live region.

In your src/lib/Pie.svelte create a new variable for the live announcement. In your <script> block, add:

let liveText = "";

This will store the text that we want the screen reader to announce.

Next, update your toggleWedge function to dynamically announce the selected slice:

function toggleWedge(index, event) {
	if (!event.key || event.key === "Enter") {
		selectedIndex = index;
		const d = data[index];
		liveText = `${d.label}: ${d.value} projects selected.`;
	}
}

(Ensure liveText is defined above the updated function)

Lastly, we can add the live region element to in the component’s HTML (e.g. after the <svg>, but before the </div>), ideally where it won’t disrupt layout:

<p aria-live="polite" class="sr-only">{liveText}</p>

And in your <style>, define a visually hidden class:

.sr-only {
  position: absolute;
  left: -9999px;
  width: 1px;
  height: 1px;
  overflow: hidden;
}

This ensures the text is read out loud but remains invisible to sighted users.

Finally, with NVDA or VoiceOver on, try selecting a slice of the pie using Tab + Enter. You should now hear something like:

“2025: 8 projects selected.”

🎉 Success!

Step 2.4: Including a Tabular Representation for Increased Accessibility

To increase the accessibility of charts, it is also a good practice to include the tabular data. This allows users who are accessing the information with a screenreader to have a way to navigate through the information without missing out on anything.

In your src/lib/Pie.svelte add the following <table> right after the <div class="container">:

<table aria-label="Table showing project counts by year" class="data-table">
    <caption>Projects by Year</caption>
    <thead>
        <tr>
          <th id="year-header" scope="col">Year</th>
          <th id="projects-header" scope="col">Projects</th>
        </tr>
      </thead>      
    <tbody>
        {#each data as d, i}
          <tr>
            <th id="row-{i}" scope="row">{d.label}</th>
            <td aria-labelledby="row-{i} projects-header">{d.value}</td>
          </tr>
        {/each}
      </tbody>
  </table>

What’s going on here?

  • <caption> gives the table a visible title (great for all users).
  • aria-label="..." describes the purpose of the table to screen readers.
  • scope="col" and scope="row" mark which headers apply to each column and row.
  • aria-labelledby="..." helps screen readers read full context, like: “2020, Projects, 15” instead of just “15”.

Add this CSS either in your <style> block:

.data-table {
  margin-top: 1rem;
  margin-bottom: 1rem;
  border-collapse: collapse;
  width: 100%;
  max-width: 30em;
}

.data-table caption {
  font-weight: bold;
  margin-bottom: 0.5em;
  text-align: left;
}

.data-table th,
.data-table td {
  border: 1px solid #ccc;
  padding: 0.5em;
  text-align: left;
}

.data-table th {
  background-color: #f0f0f0;
}

Your page should now include a table similar to this:

Step 2.5: Adding a Button to Toggle between a Tabular and Pie Chart Representation of the Data

At this point, we have both a pie chart and a tabular representation of the same data. Displaying data in a table is great for accessibility, but we may want to provide the option to view only one format at a time.

Let’s add an accessible toggle button that switches between the pie chart and the table.

In your <script> section, add the following:

    let showChart = true;

    function toggleView() {
        showChart = !showChart;
        liveText = showChart ? "Pie chart view shown." : "Table view shown.";
    }

Above <div class="container">, add the button

<button 
  on:click={toggleView}
  aria-pressed={!showChart}
  aria-label="Toggle between pie chart and table view"
  class="toggle-button">
    {showChart ? 'Show Table' : 'Show Chart'}
</button>

You should now see a button above your pie chart. Next, let’s add the functionality.

Wrap the pie chart + legend in {#if showChart} and the table in the {:else} block:

{#if showChart}
  <div class="container">
    <!-- your code for the pie chart and legend -->
  </div>
{:else}
  <table>
    <!-- your code for the table -->
  </table>
{/if}

At this point, you should be able to toggle between the pie chart and table.

Step 3: Making the Pie Chart Visually Accessible

Now, we’ve made the pie chart more accessible to screen readers. However, our chart is still not working that well from a visual accessibility point of view. We want to make the pie chart accessible for people with low vision, color vision deficiencies or other visual impairments.

You can simulate vision deficiencies in using DevTools to see how users who experience color blindness or blurred vision might see your sight. Follow the steps in the video below by Addy Osmani to preview your projects page under different conditions:

For example, a user with Achromatopsia, whom has difficulty perceiving any color except for shades of grey would see our page as the following:

A user with blurred vision might see something like the following:

Step 3.1: Ensuring sufficient color contrast

For starters, let’s use a color palette with good contrast and color-blind safe. Instead of using D3’s default colors, we can pick a color-blind safe palette, using tools like Colorbrewer and Accessible Palette Builder.

Replace let colors = d3.scaleOrdinal(d3.schemeTableau10); with:

$: colors = d3.scaleOrdinal()
    .domain(data.map((_, i) => i))
    .range(d3.quantize(d3.interpolateBlues, data.length));

If you preview your website now, you’ll see that there isn’t much contrast between the background of the site and the donut. Let’s add a border around our pie chart to make the distinction more clear.

After the svg but before each path, add this line::

    <circle class="pie-outline" r="50" />
        {#each arcs as arc, index}
        <!-- your code for the paths -->

and in your <style> section add the following:

.pie-outline {
    stroke: black;
    fill: none;
    stroke-width: 1;
}

Now users with Achromatopsia would see colors which have higher contrast:

Step 3.2: Use hover and focus styles that reinforce interaction

Your CSS already fades out non-selected paths when one is hovered or focused, which is great!

To make keyboard focus clearer, ensure the selected or focused slice is highlighted in a visible way.

In your <style> section:

path:focus-visible {
  stroke: white;
  stroke-width: 2px;
  stroke-dasharray: 4; /* Adjust the dash length as needed */
  }

This gives a visible outline to users navigating via keyboard.

Step 4: Removing elements from screenreaders visibility

Sometimes, we include elements in our visualizations or interfaces that are purely decorative or redundant—like icons, repeated labels, or helper visuals. While these elements may help sighted users, they can confuse or clutter the experience for people using screen readers.

In cases like these, we can use aria-hidden="true"attribute to hide these elements from assistive technologies.

For example, if we look at our projects page, we have a searchbar with the 🔍 emoji which is only serving a decorative purpose. This emoji will be read aloud by screen readers, which is unnecessary (and potentially confusing)!

Let’s look at our src/routes/projects/+page.svelte page. You’ll notice we have a search bar with a 🔍 emoji in the placeholder:

<input type="search" 
      bind:value={query}
      aria-label="Search projects" 
      placeholder="🔍 Search projects…" />

While the emoji helps cue sighted users that this is a search input, it’s read aloud by screen readers, which can be confusing or distracting. Unfortunately, since the emoji is part of the placeholder string, we can’t selectively hide it from screen readers.

So what do we do?

To make this more accessible, we’ll:

  1. Move the emoji out of the placeholder.
  2. Display it visually using HTML and CSS.
  3. Hide it from screen readers with aria-hidden=”true”.

First, let’s wrap the emoji in a <span> and mark it with aria-hidden="true" so assistive technologies skip it:

<span aria-hidden="true" class="emoji">🔍</span>

Putting this all together we have:

<div class="search-wrapper">
  <span aria-hidden="true" class="emoji">🔍</span>
  <input 
    type="search" 
    bind:value={query}
    aria-label="Search projects"
    placeholder="Search projects…" />
</div>

At this point, your search bar will still work—but the 🔍 won’t appear in the input. Time to bring it back visually using CSS.

In your <style> section, make the container the reference point for absolutely positioning the emoji:

.search-wrapper {
  position: relative;
}

Next, we want to positions the emoji inside the input, vertically centered:

.emoji {
  position: absolute;
  left: 0.5rem;
  top: 50%;
  transform: translateY(-50%);
  pointer-events: none;
  font-size: 1rem;
}

Finally, let’s add some padding to the left so the text input doesn’t overlap with the emoji.

input[type="search"] {
  padding-left: 2rem; /* gives space for the emoji */
  width: 100%;
}

Your search bar now should look something like this, where the 🔍 emoji is fixed in place even when you search:

🥵 That was… a lot of work for a tiny emoji!

Yep! Sometimes, making things both visually appealing and fully accessible takes a bit of extra effort. But it’s worth it— this version provides a better experience for all users: the emoji is there for sighted users, and screen reader users don’t get confused by unrelated or decorative content. Nice work! 🎉