Build Accessible UI Components

Build WCAG 2.0 compliant buttons, cards, and a modal dialog from scratch.

Introduction

30 Second Summary

Every time you click a button, fill out a form, or dismiss a popup, you rely on invisible patterns that make those interactions work for everyone. When these patterns are missing, millions of users who navigate with keyboards or screen readers hit dead ends.

In this project, you will build three accessible UI components from scratch using HTML, CSS, and JavaScript. Each component follows WCAG 2.0 compliance standards to ensure it works for all users, regardless of how they interact with the page.

What You'll Build

A Component Showcase page that displays three reusable, accessible UI components side by side, each demonstrating a different accessibility pattern.

By the end of this project, you'll have:

  • An accessible button system with visible focus indicators, proper color contrast ratios, and full keyboard activation.
  • An information card built with semantic HTML and ARIA attributes that communicate its structure to assistive technology.
  • A modal dialog using the native HTML <dialog> element with built-in focus trapping, backdrop rendering, and Escape key dismissal.
  • Secret Mission: Add a live contrast ratio calculator that programmatically verifies your components meet accessibility standards.

Are there any prerequisites?

No prior accessibility knowledge is needed. This guide walks you through setting up all required tools from scratch. You will use VS Code, Google Chrome, and Chrome DevTools throughout the project.

Set Up Your Project

Every accessible web page starts with a solid HTML foundation. The boilerplate you set up here tells the browser how to interpret your page, where to find your styles, and which language your content is written in.

These details matter for accessibility. A missing lang attribute, for example, means screen readers cannot determine the correct pronunciation rules for your text. In this step, you will set up the HTML boilerplate that makes your page accessible from the very first line of code.

In this step, get ready to:
  • Set up HTML boilerplate with accessibility-focused attributes.
  • Link your stylesheet and JavaScript file to the page.
Set up the HTML boilerplate

Your index.html file is currently empty. You will add the standard HTML structure along with attributes that assistive technologies rely on.

  • In VS Code, click on index.html in the Explorer sidebar to open it.
  • Copy the following HTML boilerplate into the file:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

What does this code do?

  • lang="en" tells screen readers to use English pronunciation rules when reading your page aloud.
  • charset="UTF-8" ensures special characters and symbols render correctly across all devices.
  • The viewport meta tag makes your page responsive on mobile devices.
  • The link and script tags connect your stylesheet and JavaScript file so the browser loads them with the page.
  • Save index.html by pressing Cmd+S (macOS) or Ctrl+S (Windows).
  • Switch to Google Chrome where Live Server is running. The page should automatically reload. You should see the title Accessible Component Showcase in the browser tab.

Browser tab still showing the filename?

  • Make sure you saved the file. Unsaved changes in VS Code show a dot on the file tab.
  • Check that your <title> element is inside the <head> section, not inside <body>.
  • If Live Server did not auto-reload, try manually refreshing with Cmd+R (macOS) or Ctrl+R (Windows).

Why isn't my browser tab showing the page title?

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

Here is the complete index.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <script src="main.js"></script>
</body>
</html>

Why does the lang attribute matter?

The lang attribute is one of the simplest accessibility wins. Without it, a screen reader has to guess which language your content is in. A French screen reader encountering an English page without lang="en" would attempt to pronounce your English words using French pronunciation rules.

Your HTML foundation is in place. Next up, you will build your first accessible component: a button system with visible focus indicators and proper contrast ratios.

Build the Accessible Button Component

Your project files are ready and Live Server is running. Now it's time to build the first component of your showcase: an accessible button system.

Buttons are the most common interactive element on any webpage. If they aren't reachable by keyboard or their text is too low-contrast against the background, some users can't interact with your interface at all. You'll build three button variants that meet WCAG 2.0 contrast requirements and work perfectly with keyboard navigation.

In this step, get ready to:
  • Create a button system with three variants using semantic HTML.
  • Set up CSS custom properties for an accessible color palette.
  • Test keyboard navigation and verify contrast ratios in Chrome DevTools.
Create the button HTML structure

Native HTML <button> elements give you keyboard activation (Enter and Space) and screen reader announcements for free. Custom <div> or <span> elements would require extra JavaScript and ARIA attributes to replicate the same behavior.

  • In VS Code, open index.html and find the opening <body> tag.
  • Replace everything between <body> and </body> (including the existing script tag) with the following code:
    <header>
        <h1>Accessible Component Showcase</h1>
        <p>Three WCAG 2.0 compliant UI components built with semantic HTML, CSS, and JavaScript.</p>
    </header>

    <main id="main-content">
        <section aria-labelledby="buttons-heading">
            <h2 id="buttons-heading">Button Components</h2>
            <div class="component-demo">
                <!-- Three button variants using native <button> elements -->
                <button class="btn btn-primary">Primary Action</button>
                <button class="btn btn-secondary">Secondary Action</button>
                <button class="btn btn-outline">Outline Action</button>
            </div>
        </section>
    </main>

    <script src="main.js"></script>

What does this code do?

The <header> and <main> elements are semantic HTML landmarks. Screen readers use these to let users jump between major sections of the page.

The <section> has aria-labelledby="buttons-heading" which links the section to its heading. This gives assistive technology a name for the region. Each <button> element is natively keyboard-accessible without any extra code.

  • Save index.html.
  • Check your browser. You should see the page heading "Accessible Component Showcase", a subtitle, a "Button Components" heading, and three plain unstyled buttons in a row.

Don't see the buttons?

Make sure Live Server is still running. Check the bottom status bar in VS Code for "Port: 5500" or similar. If the page is blank, confirm you kept the <script src="main.js"></script> tag inside the body.

Why aren't my buttons showing up in the browser?

✔️ Awesome, I've got everything!

Great. Double check you've saved index.html and move on to styling.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header>
        <h1>Accessible Component Showcase</h1>
        <p>Three WCAG 2.0 compliant UI components built with semantic HTML, CSS, and JavaScript.</p>
    </header>

    <main id="main-content">
        <section aria-labelledby="buttons-heading">
            <h2 id="buttons-heading">Button Components</h2>
            <div class="component-demo">
                <button class="btn btn-primary">Primary Action</button>
                <button class="btn btn-secondary">Secondary Action</button>
                <button class="btn btn-outline">Outline Action</button>
            </div>
        </section>
    </main>

    <script src="main.js"></script>
</body>
</html>
Set up CSS custom properties and style the buttons

CSS custom properties (also called CSS variables) let you define your color palette once and reuse it everywhere. This makes it easy to guarantee every component meets contrast requirements.

  • Open styles.css in VS Code and add the following design tokens:
:root {
    --color-primary: #1a56db;
    --color-primary-hover: #1344b0;
    --color-secondary: #4b5563;
    --color-secondary-hover: #374151;
    --color-text: #111827;
    --color-text-light: #374151;
    --color-background: #ffffff;
    --color-surface: #f9fafb;
    --color-border: #d1d5db;
    --color-focus: #1a56db;
    --color-focus-outline: #93c5fd;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius: 0.375rem;
    --font-family: system-ui, -apple-system, sans-serif;
}

What are these variables for?

The --color-primary value #1a56db (a deep blue) on #ffffff (white) gives approximately a 6.3:1 contrast ratio. That comfortably clears the WCAG 2.0 AA minimum of 4.5:1.

Spacing and radius variables keep your layout consistent. By centralizing these values, changing your design system later only requires editing one block of code.

  • You won't see a visual change yet. These variables are definitions that the next code block will consume.
  • Below the :root block, add the global page styles:
*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-background);
    line-height: 1.6;
    margin: 0;
    padding: var(--spacing-xl);
    max-width: 800px;
    margin-inline: auto;
}

:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius);
}

h1 {
    font-size: 2rem;
    margin-bottom: var(--spacing-sm);
}

h2 {
    font-size: 1.5rem;
    margin-top: var(--spacing-xl);
    margin-bottom: var(--spacing-md);
}

h3 {
    font-size: 1.25rem;
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

section {
    margin-bottom: var(--spacing-xl);
}

.component-demo {
    display: flex;
    gap: var(--spacing-md);
    flex-wrap: wrap;
    align-items: flex-start;
}

What does this code do?

The box-sizing: border-box reset ensures padding doesn't expand elements beyond their set width. The body rule applies your font, text color, and centers the page at a max width of 800px.

The :focus-visible rule is crucial for accessibility. It draws a 3px blue outline around any element that receives keyboard focus. This satisfies WCAG 2.0 SC 2.4.7 (Focus Visible), which requires a visible focus indicator on all keyboard-operable elements.

  • Save styles.css and check your browser.
  • You should see the page centered with a clean font, proper heading sizes, and your three buttons now sitting in a horizontal row (though still unstyled as default browser buttons).

Page still looks unstyled?

Confirm your <link rel="stylesheet" href="styles.css"> is in the <head> of index.html. The filename must be exactly styles.css with no typos.

Why aren't my CSS styles applying to the page?

  • Now add the button component styles below your existing CSS:
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-lg);
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--font-family);
    border-radius: var(--radius);
    border: 2px solid transparent;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
}

.btn-secondary {
    background-color: var(--color-secondary);
    color: var(--color-background);
}

.btn-secondary:hover {
    background-color: var(--color-secondary-hover);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
}

.btn-outline:hover {
    background-color: var(--color-primary);
    color: var(--color-background);
}

What does this code do?

The .btn base class provides shared layout properties. Each variant class (.btn-primary, .btn-secondary, .btn-outline) applies color combinations that all exceed the 4.5:1 contrast ratio minimum.

Each button has a distinct :hover state. Combined with the global :focus-visible outline from earlier, users can always tell which button is being targeted whether they use a mouse or a keyboard.

  • Save styles.css and check your browser.
  • You should see three polished buttons: a solid blue "Primary Action", a dark gray "Secondary Action", and a blue-outlined "Outline Action" with white text on colored backgrounds.

Buttons still look unstyled?

Check that your .btn class matches the class="btn btn-primary" in your HTML. CSS class names are case-sensitive. Also confirm there are no typos in your CSS variable references (each must start with var(--).

Why aren't my button styles showing up?

✔️ Awesome, I've got everything!

Great. Double check you've saved styles.css and move on to testing.

ⓧ I'd like to double check the full code

:root {
    --color-primary: #1a56db;
    --color-primary-hover: #1344b0;
    --color-secondary: #4b5563;
    --color-secondary-hover: #374151;
    --color-text: #111827;
    --color-text-light: #374151;
    --color-background: #ffffff;
    --color-surface: #f9fafb;
    --color-border: #d1d5db;
    --color-focus: #1a56db;
    --color-focus-outline: #93c5fd;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius: 0.375rem;
    --font-family: system-ui, -apple-system, sans-serif;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-background);
    line-height: 1.6;
    margin: 0;
    padding: var(--spacing-xl);
    max-width: 800px;
    margin-inline: auto;
}

:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius);
}

h1 {
    font-size: 2rem;
    margin-bottom: var(--spacing-sm);
}

h2 {
    font-size: 1.5rem;
    margin-top: var(--spacing-xl);
    margin-bottom: var(--spacing-md);
}

h3 {
    font-size: 1.25rem;
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

section {
    margin-bottom: var(--spacing-xl);
}

.component-demo {
    display: flex;
    gap: var(--spacing-md);
    flex-wrap: wrap;
    align-items: flex-start;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-lg);
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--font-family);
    border-radius: var(--radius);
    border: 2px solid transparent;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
}

.btn-secondary {
    background-color: var(--color-secondary);
    color: var(--color-background);
}

.btn-secondary:hover {
    background-color: var(--color-secondary-hover);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
}

.btn-outline:hover {
    background-color: var(--color-primary);
    color: var(--color-background);
}
Test keyboard navigation and verify contrast ratios

Building accessible components is only half the job. You need to verify they actually work as expected for keyboard users and pass contrast checks.

  • Click somewhere on your page in Chrome, then press **Tab** on your keyboard.
  • Watch for a blue outline (your :focus-visible style) to appear around each button as you Tab through them.
  • With a button focused, press **Enter** or **Space**. The button should activate (you'll see a brief visual click effect).
  • Confirm you can Tab forward through all three buttons and that focus never gets stuck.

Why does this work without JavaScript?

Native <button> elements respond to Enter and Space keys automatically. The browser handles keyboard activation as a built-in behavior. You only wrote CSS for the visual focus ring. This is why using semantic HTML elements is so powerful for accessibility.

Now verify the color contrast ratios using Chrome DevTools.

  • Open DevTools by pressing **Cmd+Option+I** (macOS) or **Ctrl+Shift+I** (Windows).
  • Click the **Elements** panel tab at the top of DevTools.
  • Click on the "Primary Action" button in your page to select it in the DOM tree.
  • In the **Styles** pane on the right, find the color declaration (this shows the text color).
  • Click the small colored square next to the color value to open the **Color Picker**.
  • Expand the **Contrast ratio** section in the Color Picker. You should see the computed ratio (approximately 6.3:1 for the primary button) and a green checkmark indicating it passes AA.

What is the 4.5:1 ratio?

WCAG 2.0 SC 1.4.3 requires that normal-sized text has at least a 4.5:1 contrast ratio between the text color and its background. This ensures readability for users with low vision. Your primary button uses white text (#ffffff) on a blue background (#1a56db), which exceeds this threshold.

Check all three variants

Repeat the Color Picker check on the secondary and outline buttons. The secondary button uses white on #4b5563 (approximately 7.5:1). The outline button uses #1a56db text on white (approximately 6.3:1). All pass.

Your button system is accessible and styled. Next up, you'll build a card component that uses semantic HTML to communicate its structure to assistive technology.

Build the Accessible Card Component

Your buttons look great and pass all keyboard and contrast requirements. Now you need a way to present information in a self-contained, reusable block.

In this step, you will build an information card using semantic HTML elements that tell assistive technology what each piece of content means. You will also add ARIA attributes to give the card region an accessible name.

In this step, get ready to:
  • Create a card component using semantic HTML and aria-labelledby.
  • Style the card with proper visual hierarchy and contrast compliance.
  • Verify the card's accessible name in Chrome DevTools.
Create the card with semantic HTML

A card groups related information together. Screen readers need to know where a card starts and ends, and what it is about.

  • In index.html, find the closing </section> tag of the buttons section (right after the </div> with class component-demo).
  • Add the following card section below it, still inside <main>:
        <section aria-labelledby="card-heading">
            <h2 id="card-heading">Card Component</h2>
            <article class="card" aria-labelledby="card-title">
                <h3 id="card-title">Getting Started with Accessibility</h3>
                <p>Accessible components ensure that all users, including those using screen readers or keyboard navigation, can interact with your interface effectively.</p>
                <button class="btn btn-primary">Learn More</button>
            </article>
        </section>

What does this code do?

The <article> element tells assistive technology that this is a self-contained piece of content. The aria-labelledby="card-title" attribute points to the heading's id, giving the article an accessible name.

When a screen reader encounters this card, it announces something like "article: Getting Started with Accessibility". Without aria-labelledby, the user would hear a generic "article" announcement with no context.

  • Save index.html.
  • Refresh your browser. You should see a heading "Card Component" and a block of text with a blue "Learn More" button below it.

Don't see the card section?

Make sure the new <section> is placed after the closing </section> of the buttons section, but still inside <main>. If it is outside <main>, your page structure breaks.

Check that every opening tag has a matching closing tag. A missing </article> or </section> can cause everything below it to disappear.

help me figure out why my card section isn't showing up

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header>
        <h1>Accessible Component Showcase</h1>
        <p>Three WCAG 2.0 compliant UI components built with semantic HTML, CSS, and JavaScript.</p>
    </header>

    <main id="main-content">
        <section aria-labelledby="buttons-heading">
            <h2 id="buttons-heading">Button Components</h2>
            <div class="component-demo">
                <button class="btn btn-primary">Primary Action</button>
                <button class="btn btn-secondary">Secondary Action</button>
                <button class="btn btn-outline">Outline Action</button>
            </div>
        </section>

        <section aria-labelledby="card-heading">
            <h2 id="card-heading">Card Component</h2>
            <article class="card" aria-labelledby="card-title">
                <h3 id="card-title">Getting Started with Accessibility</h3>
                <p>Accessible components ensure that all users, including those using screen readers or keyboard navigation, can interact with your interface effectively.</p>
                <button class="btn btn-primary">Learn More</button>
            </article>
        </section>
    </main>

    <script src="main.js"></script>
</body>
</html>
Style the card component

The card needs visual separation from the rest of the page. It also needs to ensure that its text color passes the WCAG 2.0 minimum contrast ratio of 4.5:1 against the card background.

  • In styles.css, add the following styles at the bottom of the file, after the .btn-outline:hover rule:
/* Card Component */
.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-lg);
    max-width: 400px;
}

.card p {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-md);
}

What does this code do?

The .card class gives the article a light background (--color-surface is #f9fafb), a subtle border, and constrained width so it looks like a contained block.

The .card p rule sets paragraph text to --color-text-light (#374151), which achieves approximately 9.6:1 contrast ratio against the #f9fafb background. This comfortably exceeds the 4.5:1 requirement.

  • Save styles.css and refresh your browser.
  • You should see the card as a contained box with a light grey background, a thin border, and clear spacing between the heading, paragraph, and button inside it.

Card looks the same as before?

Make sure the class card in your CSS matches the class class="card" in your HTML exactly. Class names are case-sensitive.

Check that the CSS rules are not inside another selector's closing brace. They should be top-level rules, not nested inside another block.

help me debug why my card styles aren't applying

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

:root {
    --color-primary: #1a56db;
    --color-primary-hover: #1344b0;
    --color-secondary: #4b5563;
    --color-secondary-hover: #374151;
    --color-text: #111827;
    --color-text-light: #374151;
    --color-background: #ffffff;
    --color-surface: #f9fafb;
    --color-border: #d1d5db;
    --color-focus: #1a56db;
    --color-focus-outline: #93c5fd;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius: 0.375rem;
    --font-family: system-ui, -apple-system, sans-serif;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-background);
    line-height: 1.6;
    margin: 0;
    padding: var(--spacing-xl);
    max-width: 800px;
    margin-inline: auto;
}

:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius);
}

h1 {
    font-size: 2rem;
    margin-bottom: var(--spacing-sm);
}

h2 {
    font-size: 1.5rem;
    margin-top: var(--spacing-xl);
    margin-bottom: var(--spacing-md);
}

h3 {
    font-size: 1.25rem;
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

section {
    margin-bottom: var(--spacing-xl);
}

.component-demo {
    display: flex;
    gap: var(--spacing-md);
    flex-wrap: wrap;
    align-items: flex-start;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-lg);
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--font-family);
    border-radius: var(--radius);
    border: 2px solid transparent;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
}

.btn-secondary {
    background-color: var(--color-secondary);
    color: var(--color-background);
}

.btn-secondary:hover {
    background-color: var(--color-secondary-hover);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
}

.btn-outline:hover {
    background-color: var(--color-primary);
    color: var(--color-background);
}

/* Card Component */
.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-lg);
    max-width: 400px;
}

.card p {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-md);
}
Verify accessibility in Chrome DevTools

The card looks right visually. Now you need to confirm that assistive technology can read it correctly by checking the accessibility tree.

  • Right-click the card on your page and select **Inspect** to open Chrome DevTools.
  • In the **Elements** panel, click on the <article> element in the DOM tree.
  • Click the **Accessibility** tab in the sidebar. This tab may be hidden behind the **More Tabs** button.
  • Look at the accessibility tree entry for the article. You should see it has the role "article" and an accessible name of "Getting Started with Accessibility" (pulled from the heading via aria-labelledby).

What is the accessibility tree?

The accessibility tree is a simplified version of the DOM that the browser builds specifically for assistive technology like screen readers. It strips out purely visual elements and keeps only the content that carries meaning.

When you verify your article in this tree, you confirm that a screen reader user will hear "article: Getting Started with Accessibility" when they reach your card.

  • While still in DevTools, click the <p> element inside the card in the DOM tree.
  • Switch to the **Styles** tab and look at the color property. Hover over the color swatch to see the contrast ratio displayed.
  • Confirm the ratio shows 4.5:1 or higher. The #374151 text on #f9fafb background achieves approximately 9.6:1.

Can't find the Accessibility tab?

The Accessibility tab may be hidden. Look for a >> (More Tabs) icon in the sidebar area of the Elements panel. Click it to reveal additional tabs including Accessibility.

If you still do not see it, make sure you have selected an element in the DOM tree first. The tab only shows content when a node is selected.

help me find the Accessibility tab in Chrome DevTools

Your card is both visually polished and fully accessible. Next up, you will build a modal dialog using the native HTML <dialog> element, which gives you built-in focus trapping and keyboard dismissal for free.

Build the Accessible Modal Dialog

Your buttons and card are looking great. Now it's time to tackle the trickiest component in the set: a modal dialog.

Modals are notoriously hard to make accessible because keyboard focus needs to stay trapped inside them. Users who rely on the Tab key should never accidentally end up back on the page behind the modal. The native HTML <dialog> element solves this problem by giving you built-in focus trapping, a backdrop overlay, and Escape key dismissal without any custom JavaScript workarounds.

In this step, get ready to:
  • Create a modal dialog with the native HTML <dialog> element and proper ARIA attributes.
  • Write JavaScript to open and close the dialog using showModal() and close().
  • Test the full keyboard accessibility flow including focus trapping and Escape dismissal.
Create the dialog HTML structure

The <dialog> element is a purpose-built HTML tag for popups and modals. When you open it with showModal(), the browser automatically traps focus inside, renders a backdrop, and lets the user press Escape to close it.

  • In index.html, find the closing </section> tag of the card section (the one right before </main>).
  • Add the following new section below it, before the closing </main> tag:
        <section aria-labelledby="modal-heading">
            <h2 id="modal-heading">Modal Dialog Component</h2>
            <!-- Trigger button tells assistive tech a dialog will open -->
            <button class="btn btn-primary" id="open-modal" aria-haspopup="dialog">Open Modal Dialog</button>

            <!-- Native dialog element with accessible name from heading -->
            <dialog id="demo-modal" aria-labelledby="modal-title">
                <h3 id="modal-title">Confirmation Dialog</h3>
                <p>This modal uses the native HTML dialog element. Focus is trapped inside, and you can press Escape to close it.</p>
                <div class="modal-actions">
                    <button class="btn btn-primary" id="modal-confirm">Confirm</button>
                    <button class="btn btn-outline" id="modal-close">Cancel</button>
                </div>
            </dialog>
        </section>

What does this code do?

  • aria-haspopup="dialog" on the trigger button tells screen readers that clicking this button will open a dialog popup.
  • The <dialog> element is hidden by default. It only appears when opened with JavaScript.
  • aria-labelledby="modal-title" gives the dialog an accessible name by pointing to the heading inside it. Screen readers announce this name when focus enters the dialog.
  • The .modal-actions div groups the confirm and cancel buttons together at the bottom.

Why the native dialog element?

Before <dialog> existed, developers had to build modals from <div> elements and write hundreds of lines of JavaScript to handle focus trapping, backdrop clicks, and Escape key behavior. Every custom implementation had edge cases that broke accessibility.

The native <dialog> element gives you all of that behavior for free. When opened with showModal(), the browser makes everything behind it inert, traps focus inside, and handles keyboard dismissal. Less code, fewer bugs, better accessibility.

  • Save index.html.
  • Refresh your browser. You should see a new "Modal Dialog Component" heading with an "Open Modal Dialog" blue button below it.

Don't see the button?

  • Make sure you pasted the new section between the card section's closing </section> tag and the closing </main> tag.
  • Check that all tags are properly closed. A missing </section> or </dialog> can break the page layout.

Why isn't my modal section showing up on the page?

The dialog itself won't be visible yet. That's expected. The <dialog> element stays hidden until you open it with JavaScript.

  • Now add the dialog styles. In styles.css, scroll to the bottom of the file (after the .card p rule) and add the following styles:
/* Modal Dialog */
dialog {
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-xl);
    max-width: 480px;
    width: 90%;
}

dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
}

.modal-actions {
    display: flex;
    gap: var(--spacing-md);
    margin-top: var(--spacing-lg);
    justify-content: flex-end;
}

What does this code do?

  • The dialog rule styles the dialog box itself with a border, rounded corners, and padding. max-width: 480px keeps it from stretching too wide on large screens.
  • dialog::backdrop styles the semi-transparent dark overlay that appears behind the modal. The browser creates this backdrop automatically when you use showModal().
  • .modal-actions uses flexbox to push the Confirm and Cancel buttons to the right side, matching common dialog patterns.
  • Save styles.css.
  • You won't see a change yet. The dialog styles only take effect once the dialog is opened with JavaScript in the next substep.

✔️ Awesome, I've got everything!

Great. Double check you've saved both index.html and styles.css.

ⓧ I'd like to double check the full code

Here is the full index.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <header>
        <h1>Accessible Component Showcase</h1>
        <p>Three WCAG 2.0 compliant UI components built with semantic HTML, CSS, and JavaScript.</p>
    </header>

    <main id="main-content">
        <section aria-labelledby="buttons-heading">
            <h2 id="buttons-heading">Button Components</h2>
            <div class="component-demo">
                <button class="btn btn-primary">Primary Action</button>
                <button class="btn btn-secondary">Secondary Action</button>
                <button class="btn btn-outline">Outline Action</button>
            </div>
        </section>

        <section aria-labelledby="card-heading">
            <h2 id="card-heading">Card Component</h2>
            <article class="card" aria-labelledby="card-title">
                <h3 id="card-title">Getting Started with Accessibility</h3>
                <p>Accessible components ensure that all users, including those using screen readers or keyboard navigation, can interact with your interface effectively.</p>
                <button class="btn btn-primary">Learn More</button>
            </article>
        </section>

        <section aria-labelledby="modal-heading">
            <h2 id="modal-heading">Modal Dialog Component</h2>
            <button class="btn btn-primary" id="open-modal" aria-haspopup="dialog">Open Modal Dialog</button>

            <dialog id="demo-modal" aria-labelledby="modal-title">
                <h3 id="modal-title">Confirmation Dialog</h3>
                <p>This modal uses the native HTML dialog element. Focus is trapped inside, and you can press Escape to close it.</p>
                <div class="modal-actions">
                    <button class="btn btn-primary" id="modal-confirm">Confirm</button>
                    <button class="btn btn-outline" id="modal-close">Cancel</button>
                </div>
            </dialog>
        </section>
    </main>

    <script src="main.js"></script>
</body>
</html>

And here is the full styles.css at this point:

:root {
    --color-primary: #1a56db;
    --color-primary-hover: #1344b0;
    --color-secondary: #4b5563;
    --color-secondary-hover: #374151;
    --color-text: #111827;
    --color-text-light: #374151;
    --color-background: #ffffff;
    --color-surface: #f9fafb;
    --color-border: #d1d5db;
    --color-focus: #1a56db;
    --color-focus-outline: #93c5fd;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius: 0.375rem;
    --font-family: system-ui, -apple-system, sans-serif;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-background);
    line-height: 1.6;
    margin: 0;
    padding: var(--spacing-xl);
    max-width: 800px;
    margin-inline: auto;
}

:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius);
}

h1 {
    font-size: 2rem;
    margin-bottom: var(--spacing-sm);
}

h2 {
    font-size: 1.5rem;
    margin-top: var(--spacing-xl);
    margin-bottom: var(--spacing-md);
}

h3 {
    font-size: 1.25rem;
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

section {
    margin-bottom: var(--spacing-xl);
}

.component-demo {
    display: flex;
    gap: var(--spacing-md);
    flex-wrap: wrap;
    align-items: flex-start;
}

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-lg);
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--font-family);
    border-radius: var(--radius);
    border: 2px solid transparent;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
}

.btn-secondary {
    background-color: var(--color-secondary);
    color: var(--color-background);
}

.btn-secondary:hover {
    background-color: var(--color-secondary-hover);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
}

.btn-outline:hover {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-lg);
    max-width: 400px;
}

.card p {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-md);
}

dialog {
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-xl);
    max-width: 480px;
    width: 90%;
}

dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
}

.modal-actions {
    display: flex;
    gap: var(--spacing-md);
    margin-top: var(--spacing-lg);
    justify-content: flex-end;
}
Write JavaScript to control the dialog

The dialog HTML is in place, but it needs JavaScript to open and close. You'll use showModal() to open it (which enables all the accessibility features) and close() to dismiss it.

  • Add the modal interaction code to main.js by pasting the following:
// Modal Dialog Interaction
const openModalButton = document.getElementById('open-modal');
const modal = document.getElementById('demo-modal');
const modalConfirmButton = document.getElementById('modal-confirm');
const modalCloseButton = document.getElementById('modal-close');

openModalButton.addEventListener('click', () => {
    modal.showModal();
});

modalConfirmButton.addEventListener('click', () => {
    modal.close();
});

modalCloseButton.addEventListener('click', () => {
    modal.close();
});

What does this code do?

  • The first four lines grab references to the trigger button, dialog, and both action buttons using their id attributes.
  • modal.showModal() opens the dialog as a modal. This is different from modal.show() because showModal() adds the backdrop, traps focus, and makes the rest of the page inert.
  • modal.close() dismisses the dialog. Both the Confirm and Cancel buttons call this so the user always has a clear exit.
  • The browser also handles Escape key dismissal automatically. You don't need to write any keyboard event listeners for that.
  • Save main.js and refresh your browser.
  • Click the Open Modal Dialog button. You should see a centered dialog appear with a dark backdrop behind it, containing a heading, a paragraph, and two action buttons.
  • Click Cancel or Confirm to close the dialog.

Nothing happens when you click the button?

  • Open Chrome DevTools (Cmd+Option+I on macOS, Ctrl+Shift+I on Windows) and check the Console tab for errors.
  • Verify that the id values in your HTML (open-modal, demo-modal, modal-confirm, modal-close) match the strings in your JavaScript exactly.
  • Make sure the <script src="main.js"></script> tag is at the bottom of your <body>, after the HTML elements it references.

My modal dialog isn't opening when I click the trigger button. Help me debug this.

✔️ Awesome, I've got everything!

Great. Double check you've saved main.js.

ⓧ I'd like to double check the full code

Here is the full main.js at this point:

// Modal Dialog Interaction
const openModalButton = document.getElementById('open-modal');
const modal = document.getElementById('demo-modal');
const modalConfirmButton = document.getElementById('modal-confirm');
const modalCloseButton = document.getElementById('modal-close');

openModalButton.addEventListener('click', () => {
    modal.showModal();
});

modalConfirmButton.addEventListener('click', () => {
    modal.close();
});

modalCloseButton.addEventListener('click', () => {
    modal.close();
});
Test the full accessibility flow

The modal works with mouse clicks, but accessible components must also work entirely via keyboard. Time to verify that focus trapping, keyboard dismissal, and focus restoration all function correctly.

  • Click somewhere on the page background to clear any current focus.
  • Press Tab repeatedly until focus reaches the Open Modal Dialog button. You should see the blue focus ring appear around it.
  • Press Enter to open the dialog. The modal should appear with the dark backdrop.
  • Press Tab inside the dialog. Focus should move between the Confirm and Cancel buttons. It should NOT escape to the page behind the modal.
  • Press Escape to close the dialog.
  • Verify that focus returns to the Open Modal Dialog button (the blue focus ring should be back on it).

Why does focus return to the trigger button?

When a modal closes, the user needs to end up back where they started. If focus jumped somewhere random, a keyboard user would be lost on the page. The native <dialog> element handles this automatically. Focus returns to whichever element was focused before the dialog opened.

Focus escapes the modal when you Tab?

  • Make sure you're opening the dialog with showModal() and not show(). Only showModal() enables focus trapping and the backdrop.
  • Confirm you're using the <dialog> element (not a <div>). Only the native dialog element gets browser-level focus management.

Focus is escaping my modal dialog when I press Tab. How do I fix this?

Your modal is fully functional and accessible. All of that focus trapping, backdrop rendering, and keyboard dismissal came from the browser itself. Next up, you'll bring all three components together on a polished showcase page with proper landmarks and a skip-to-content link.

Assemble the Component Showcase Page

You've built three accessible components that each pass contrast and keyboard requirements. Now it's time to make the page itself work as a cohesive, keyboard-navigable experience from top to bottom.

One critical accessibility feature is still missing. Keyboard users currently have to Tab through every single element before reaching the main content. A skip-to-content link solves this by letting them jump straight past the header.

In this step, get ready to:
  • Review the page layout and landmark structure already in place.
  • Add a skip-to-content link for keyboard navigation.
  • Complete a full keyboard-only walkthrough to verify accessibility.
Review your page layout and landmarks

Your page already has a solid landmark structure from the previous steps. Before adding new features, take a moment to confirm what's in place.

  • Switch back to index.html in VS Code.
  • Confirm your page has a <main id="main-content"> landmark wrapping all three component sections.
  • Confirm each <section> has an aria-labelledby attribute pointing to its heading's id.

What is a landmark?

Landmarks are HTML elements that define major regions of a page. Screen readers use them to let users jump between sections quickly.

Your page uses <header>, <main>, and <section> elements as landmarks. Each one communicates a clear region to assistive technology without needing extra ARIA roles.

  • In Chrome, right-click the page and select Inspect to open DevTools.
  • Click the Accessibility tab in the right panel (you may need to click the >> arrows to find it).
  • Select the <main> element in the Elements panel. The accessibility tree should show it as a main landmark.
Add a skip-to-content link

A skip-to-content link is an anchor that appears as the very first focusable element on the page. It stays visually hidden until a keyboard user presses Tab, then jumps them directly to the <main> content.

Why add a skip link?

WCAG 2.0 Success Criterion 2.4.1 requires a mechanism to bypass blocks of repeated content. Without a skip link, keyboard users must Tab through every header element on every page visit before reaching the main content.

  • In index.html, find the opening <body> tag.
  • Add this skip link as the very first element inside <body>, before the <header>:
    <a href="#main-content" class="skip-link">Skip to main content</a>

What does this code do?

  • The href="#main-content" points to your existing <main id="main-content"> element. Clicking the link moves focus directly there.
  • The class="skip-link" will let you style it to be hidden off-screen until focused.

The skip link needs CSS to hide it visually but keep it accessible to keyboard users. When focused, it slides into view.

  • Switch to styles.css.
  • Find the closing brace of your body rule. Add the following skip link styles below it, before the :focus-visible rule:
/* Skip Link */
.skip-link {
    position: absolute;
    top: -100%;
    left: var(--spacing-md);
    background: var(--color-primary);
    color: var(--color-background);
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 600;
    z-index: 1000;
}

.skip-link:focus {
    top: var(--spacing-md);
}

What does this code do?

  • top: -100% positions the link completely off-screen. Sighted users never see it during normal browsing.
  • .skip-link:focus sets top: var(--spacing-md), which slides the link into view only when a keyboard user focuses it with Tab.
  • z-index: 1000 ensures the link appears above all other content when visible.
  • Save both index.html and styles.css.
  • In your browser, click anywhere on the page, then press Tab. You should see a blue "Skip to main content" link appear in the top-left corner of the page.

Don't see the skip link when you Tab?

  • Make sure you saved both files. The HTML adds the element and the CSS controls its visibility.
  • Verify the skip link is the first element inside <body>, before <header>. If it comes after other elements, Tab will focus those first.
  • Check that your CSS uses .skip-link:focus (not :focus-visible) because links show focus on both click and keyboard by default.

Why isn't my skip link appearing when I press Tab?

✔️ Awesome, I've got everything!

Great. Make sure you've saved both files before continuing to the keyboard walkthrough.

ⓧ I'd like to double check the full code

Here's the complete index.html at this point:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Accessible Component Showcase</title>
    <link rel="stylesheet" href="styles.css">
</head>
<body>
    <a href="#main-content" class="skip-link">Skip to main content</a>

    <header>
        <h1>Accessible Component Showcase</h1>
        <p>Three WCAG 2.0 compliant UI components built with semantic HTML, CSS, and JavaScript.</p>
    </header>

    <main id="main-content">
        <section aria-labelledby="buttons-heading">
            <h2 id="buttons-heading">Button Components</h2>
            <div class="component-demo">
                <button class="btn btn-primary">Primary Action</button>
                <button class="btn btn-secondary">Secondary Action</button>
                <button class="btn btn-outline">Outline Action</button>
            </div>
        </section>

        <section aria-labelledby="card-heading">
            <h2 id="card-heading">Card Component</h2>
            <article class="card" aria-labelledby="card-title">
                <h3 id="card-title">Getting Started with Accessibility</h3>
                <p>Accessible components ensure that all users, including those using screen readers or keyboard navigation, can interact with your interface effectively.</p>
                <button class="btn btn-primary">Learn More</button>
            </article>
        </section>

        <section aria-labelledby="modal-heading">
            <h2 id="modal-heading">Modal Dialog Component</h2>
            <button class="btn btn-primary" id="open-modal" aria-haspopup="dialog">Open Modal Dialog</button>

            <dialog id="demo-modal" aria-labelledby="modal-title">
                <h3 id="modal-title">Confirmation Dialog</h3>
                <p>This modal uses the native HTML dialog element. Focus is trapped inside, and you can press Escape to close it.</p>
                <div class="modal-actions">
                    <button class="btn btn-primary" id="modal-confirm">Confirm</button>
                    <button class="btn btn-outline" id="modal-close">Cancel</button>
                </div>
            </dialog>
        </section>
    </main>

    <script src="main.js"></script>
</body>
</html>

And the complete styles.css:

:root {
    --color-primary: #1a56db;
    --color-primary-hover: #1344b0;
    --color-secondary: #4b5563;
    --color-secondary-hover: #374151;
    --color-text: #111827;
    --color-text-light: #374151;
    --color-background: #ffffff;
    --color-surface: #f9fafb;
    --color-border: #d1d5db;
    --color-focus: #1a56db;
    --color-focus-outline: #93c5fd;
    --spacing-xs: 0.25rem;
    --spacing-sm: 0.5rem;
    --spacing-md: 1rem;
    --spacing-lg: 1.5rem;
    --spacing-xl: 2rem;
    --radius: 0.375rem;
    --font-family: system-ui, -apple-system, sans-serif;
}

*,
*::before,
*::after {
    box-sizing: border-box;
}

body {
    font-family: var(--font-family);
    color: var(--color-text);
    background-color: var(--color-background);
    line-height: 1.6;
    margin: 0;
    padding: var(--spacing-xl);
    max-width: 800px;
    margin-inline: auto;
}

/* Skip Link */
.skip-link {
    position: absolute;
    top: -100%;
    left: var(--spacing-md);
    background: var(--color-primary);
    color: var(--color-background);
    padding: var(--spacing-sm) var(--spacing-md);
    border-radius: var(--radius);
    text-decoration: none;
    font-weight: 600;
    z-index: 1000;
}

.skip-link:focus {
    top: var(--spacing-md);
}

/* Global Focus Styles */
:focus-visible {
    outline: 3px solid var(--color-focus);
    outline-offset: 2px;
    border-radius: var(--radius);
}

/* Headings */
h1 {
    font-size: 2rem;
    margin-bottom: var(--spacing-sm);
}

h2 {
    font-size: 1.5rem;
    margin-top: var(--spacing-xl);
    margin-bottom: var(--spacing-md);
}

h3 {
    font-size: 1.25rem;
    margin-top: 0;
    margin-bottom: var(--spacing-sm);
}

/* Sections */
section {
    margin-bottom: var(--spacing-xl);
}

.component-demo {
    display: flex;
    gap: var(--spacing-md);
    flex-wrap: wrap;
    align-items: flex-start;
}

/* Button Component */
.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    padding: var(--spacing-sm) var(--spacing-lg);
    font-size: 1rem;
    font-weight: 600;
    font-family: var(--font-family);
    border-radius: var(--radius);
    border: 2px solid transparent;
    cursor: pointer;
    transition: background-color 0.2s, border-color 0.2s, color 0.2s;
}

.btn-primary {
    background-color: var(--color-primary);
    color: var(--color-background);
}

.btn-primary:hover {
    background-color: var(--color-primary-hover);
}

.btn-secondary {
    background-color: var(--color-secondary);
    color: var(--color-background);
}

.btn-secondary:hover {
    background-color: var(--color-secondary-hover);
}

.btn-outline {
    background-color: transparent;
    color: var(--color-primary);
    border-color: var(--color-primary);
}

.btn-outline:hover {
    background-color: var(--color-primary);
    color: var(--color-background);
}

/* Card Component */
.card {
    background-color: var(--color-surface);
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-lg);
    max-width: 400px;
}

.card p {
    color: var(--color-text-light);
    margin-bottom: var(--spacing-md);
}

/* Modal Dialog */
dialog {
    border: 1px solid var(--color-border);
    border-radius: var(--radius);
    padding: var(--spacing-xl);
    max-width: 480px;
    width: 90%;
}

dialog::backdrop {
    background-color: rgba(0, 0, 0, 0.5);
}

.modal-actions {
    display: flex;
    gap: var(--spacing-md);
    margin-top: var(--spacing-lg);
    justify-content: flex-end;
}
Do a full keyboard-only walkthrough

The real test of an accessible page is whether someone can use it entirely without a mouse. You'll now walk through the complete page using only your keyboard.

  • Click the browser's address bar (to move focus out of the page), then press Tab once. The "Skip to main content" link should appear at the top-left.
  • Press Enter to activate the skip link. Focus should jump to the <main> area.
  • Press Tab repeatedly to move through all interactive elements. The focus order should be: Primary Action, Secondary Action, Outline Action, Learn More, Open Modal Dialog.
  • With focus on the Open Modal Dialog button, press Enter to open the modal.
  • Inside the modal, press Tab. Focus should cycle between the Confirm and Cancel buttons only. It should NOT escape to the page behind.
  • Press Escape to close the modal. Focus should return to the Open Modal Dialog button.

Why does focus return to the trigger button?

When a modal closes, the browser returns focus to the element that opened it. This is built-in behavior of the native <dialog> element with showModal(). Without this, a keyboard user would lose their place on the page after closing a dialog.

Focus escaping the modal or not returning?

  • If Tab escapes the modal, make sure you're opening it with modal.showModal() in your JavaScript, not modal.show(). Only showModal() provides focus trapping.
  • If focus does not return to the trigger button after closing, check that you are using the native <dialog> element (not a <div> with a role). The browser handles focus restoration automatically.

Help me debug why focus is not behaving correctly with my dialog element.

Secret mission

Add a Focus-Visible System and Live Contrast Display

Your components pass contrast checks when you inspect them manually in DevTools. But what if your page could check itself? In this mission, you'll write JavaScript that programmatically calculates contrast ratios using the official WCAG luminance formula and displays the results live on the page.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your resources as they are or delete them entirely.

Resources you used:

  • Project files: index.html, styles.css, and main.js.
  • Live Server VS Code extension.
  • Google Chrome browser (used for testing accessibility).

Keep everything running

No action needed. Choose this if you plan to keep building on these components or want to reference them later.

  • Your project files are saved locally on your Desktop. They take up minimal disk space.
  • The Live Server extension stays installed in VS Code for future projects.
  • You can reopen the folder in VS Code any time to continue working.

Pause - I'll come back to this later

Stop the Live Server to free up the local port it uses. Your files stay intact.

  • In VS Code, click Go Live in the bottom status bar to stop the server. The button text changes to Port: 5500 while running.
  • Close VS Code when you are done.
  • Your files remain on your Desktop. Reopen the folder in VS Code and click Go Live again when you are ready to resume.

Delete - I don't want to use this again

Remove all project files and optionally uninstall the Live Server extension.

  • Stop Live Server by clicking Port: 5500 in the VS Code status bar.
  • Delete the project folder from your Desktop. Right-click the folder in Finder (macOS) or File Explorer (Windows) and select Move to Trash or Delete.
  • To uninstall the Live Server extension, open VS Code, click the Extensions icon in the left sidebar, find Live Server in your installed list, right-click it, and select Uninstall.

Nice Work!

Nice Work!

You just built a fully accessible component showcase from scratch using semantic HTML, CSS custom properties, and vanilla JavaScript.

You've learned how to:

  • Build three accessible UI components (button, card, modal) using semantic HTML elements that communicate meaning to assistive technology.
  • Apply WCAG 2.0 Level AA contrast requirements (4.5:1 minimum ratio) and verify them using Chrome DevTools.
  • Use the native HTML <dialog> element with showModal() for built-in focus trapping, backdrop rendering, and Escape key dismissal.
  • Secret Mission: Built a programmatic contrast ratio calculator using the WCAG luminance formula to display live PASS/FAIL badges beneath each component.

Ready to quiz yourself?