Chapter 27 — Front Matter
Chapter Introduction and Learning Objectives
You have spent the last two chapters building multi-page sites with shared navigation, headers, and footers. Your pages have structure. Your site has consistency. Now you need to fill those pages with well-designed, usable, and accessible content components.
This chapter introduces user interface patterns: repeatable solutions to common content problems. A card grid is a pattern. An image gallery is a pattern. An accordion that hides and shows answers to frequently asked questions is a pattern. A set of tabs that lets users switch between sections without leaving the page is a pattern.
Every website you visit uses these patterns. They work because they solve recurring problems in clear, learnable ways. Users recognize them. Developers can build and maintain them. When done well, they also work for keyboard users and screen reader users.
Why This Chapter Matters
This chapter brings together HTML structure, CSS layout, and JavaScript interaction into real working components. Each pattern you learn here — cards, galleries, accordions, and tabs — can be carried directly into your running project, and each one is built with accessibility in mind from the very first line of code rather than added on as an afterthought.
10Sections
7Objectives
21Key Terms
30Total Chapters
Learning Objectives
By the end of this chapter, you should be able to:
- 1Build card grids that display repeated content cleanly using CSS Grid and Flexbox.
- 2Create image galleries that display a collection of photos with proper semantic markup and accessible alt text.
- 3Build an accordion that shows and hides content on demand, including correct ARIA state attributes.
- 4Build a tab interface that lets users switch between panels using the ARIA tab pattern and arrow-key navigation.
- 5Use class toggling with the
classList API to control visibility and state with JavaScript.
- 6Think about keyboard interaction and accessible patterns from the start, not as an afterthought.
- 7Choose UI patterns that fit the content and the user's needs rather than defaulting to whatever looks fashionable.
Sections 27.1 – 27.5
Patterns, Cards, Galleries, and Show/Hide Behavior
UI Patterns · Card Grids · Image Galleries · Toggling
Section 27.1
What UI Patterns Are
A user interface pattern, often called a UI pattern, is a proven, reusable solution to a common design and interaction problem.
Patterns are not invented from scratch every time a developer builds a website. They are learned from existing websites, refined over many years, and shared across the web development community. A card grid, an accordion, a modal dialog, a tab interface — these are patterns that millions of websites use because they work.
Why Patterns Matter
Reason 1
Users Already Know Them
When a user sees a row of cards, they understand they can click on one. When they see a row of tabs, they understand they can click to switch sections. Familiarity reduces confusion.
Reason 2
Developers Can Build and Improve Them
Because patterns are well understood, developers can find clear instructions, fix known problems, and improve them over time instead of reinventing the wheel.
Reason 3
Patterns Can Be Made Accessible
Because patterns are studied and documented, the accessibility community has worked out how they should behave for keyboard users and screen reader users.
Reason 4
Patterns Stay Maintainable
A well-built card component, accordion, or tab interface can be updated later without breaking the rest of the page.
A pattern is a starting point, not a strict template. You can adapt the visual style of a card to fit your site. You can change the colors of a tab interface. You can expand or simplify an accordion. The structure and the behavior should remain recognizable, but the appearance is yours to design.
The Patterns in This Chapter
This chapter covers five core patterns that appear constantly in real web development:
- Card grids — displaying repeated content items in a structured grid
- Image galleries — displaying a collection of images in a browsable layout
- Show and hide behavior — toggling content visibility with JavaScript
- Accordions — a series of collapsible content panels
- Tabs — a set of labeled panels where only one panel is visible at a time
Each pattern uses HTML, CSS, and JavaScript together. You will see the full code for each one, learn how it works, and practice building your own.
Section 27.2
Card Grids
A card is a self-contained content block that represents one item. A card typically includes a title, a short description, sometimes an image, and sometimes a link or button. Cards are grouped into a card grid: a layout that arranges multiple cards into rows and columns.
You have already used cards in earlier chapters. This section treats them more completely, including the HTML structure, the CSS grid layout, the hover behavior, and the responsive adjustments.
The Card HTML Structure
A single card is an <article> element (or a <div> when the content is not independently meaningful). It contains the card's internal content.
HTML — a single card
<article class="card">
<img src="images/lesson-html.jpg" alt="A text editor showing HTML code">
<div class="card-body">
<h3 class="card-title">Introduction to HTML</h3>
<p class="card-description">
Learn how HTML documents are structured, how elements and attributes work,
and how to write your first web page.
</p>
<a href="lessons/html.html" class="card-link">Start lesson</a>
</div>
</article>
Each part of the card has its own class. The outer element gets card. The text content area gets card-body. The heading gets card-title. The description gets card-description. The link gets card-link.
Using these specific classes makes your CSS much more predictable. You can style .card-title without accidentally affecting other headings on the page.
The Card Grid Wrapper
Cards live inside a grid wrapper:
HTML — the grid wrapper
<section class="card-grid" aria-labelledby="lessons-heading">
<h2 id="lessons-heading">All Lessons</h2>
<div class="cards">
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
<article class="card">...</article>
</div>
</section>
The <section> wraps the whole pattern and carries a label through aria-labelledby. This gives screen reader users a clear landmark for the cards section.
CSS for the Card Grid
The grid layout uses display: grid with repeat() and minmax() so the columns adjust automatically as the screen width changes:
CSS — the grid container
.cards {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
}
auto-fill tells the grid to fit as many columns as possible. minmax(280px, 1fr) means each column is at least 280 pixels wide and can grow to fill available space. On a wide screen you get three or four columns. On a tablet you get two. On a narrow phone you get one. No media queries are needed for this basic adaptation.
CSS — the card itself
.card {
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.card:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
transform: translateY(-2px);
}
.card img {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
display: block;
}
.card-body {
padding: 1.25rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
flex: 1;
}
.card-title {
font-size: 1.125rem;
font-weight: 600;
margin: 0;
}
.card-description {
font-size: 0.9375rem;
line-height: 1.6;
color: var(--color-text-secondary);
margin: 0;
flex: 1;
}
.card-link {
align-self: flex-start;
font-weight: 600;
color: var(--color-accent);
text-decoration: none;
border-bottom: 2px solid transparent;
transition: border-color 0.15s ease;
}
.card-link:hover {
border-bottom-color: var(--color-accent);
}
There are a few important techniques here.
flex-direction: column on .card stacks the image and card body vertically. flex: 1 on .card-body makes the body grow to fill available space, so all cards in a row reach the same height regardless of how much text they contain. flex: 1 on .card-description pushes the link to the bottom of the card body.
aspect-ratio: 16 / 9 on the image forces a consistent image shape. object-fit: cover crops the image to fill that shape without distorting it.
The hover state with transform: translateY(-2px) gives a subtle lift effect when the user hovers over a card.
Cards Without Images
Not every card needs an image. When your content is text-only, simply omit the <img> tag:
HTML — a text-only card
<article class="card">
<div class="card-body">
<h3 class="card-title">HTML Fundamentals</h3>
<p class="card-description">
Structure, elements, attributes, and document organization.
</p>
<a href="lessons/html.html" class="card-link">View topic</a>
</div>
</article>
The CSS works the same way. The card body fills the full card when there is no image.
Cards as Links
Sometimes the entire card should be clickable, not just the link inside it. This is common for content cards where every element leads to the same destination. You can wrap the card in an <a> element:
HTML — a fully clickable card
<a href="lessons/html.html" class="card">
<img src="images/lesson-html.jpg" alt="HTML lesson preview">
<div class="card-body">
<h3 class="card-title">Introduction to HTML</h3>
<p class="card-description">
Learn how HTML documents are structured and how to write your first page.
</p>
</div>
</a>
When you do this, the <a> element takes the role of the card. Change the CSS to apply the card styles to a.card instead. Be careful with display: block on the <a> element so it fills its grid cell correctly.
A Caution
If you make the whole card a link, the link text should still be meaningful. Screen readers will announce the link text as the image's alt text followed by the heading and description. Test this to make sure it reads naturally rather than as a confusing jumble of repeated or contradictory information.
Section 27.3
Image Galleries
An image gallery displays a collection of photographs, illustrations, or other images in a browsable grid. Galleries appear on portfolio sites, product pages, event pages, and any site that needs to present many images together.
A Basic CSS Grid Gallery
The simplest form of gallery is a CSS grid of images with no JavaScript:
HTML — a basic gallery
<section class="gallery" aria-label="Project screenshots">
<h2>Project Screenshots</h2>
<div class="gallery-grid">
<figure class="gallery-item">
<img src="images/project-1.jpg" alt="Home page of the student learning site showing a hero section and card grid">
<figcaption>Home page</figcaption>
</figure>
<figure class="gallery-item">
<img src="images/project-2.jpg" alt="Lessons page showing a list of topic cards">
<figcaption>Lessons page</figcaption>
</figure>
<figure class="gallery-item">
<img src="images/project-3.jpg" alt="Contact form with name, email, and message fields">
<figcaption>Contact page</figcaption>
</figure>
<figure class="gallery-item">
<img src="images/project-4.jpg" alt="Resources page showing a filterable list of links">
<figcaption>Resources page</figcaption>
</figure>
<figure class="gallery-item">
<img src="images/project-5.jpg" alt="Gallery page showing a grid of screenshots">
<figcaption>Gallery page</figcaption>
</figure>
<figure class="gallery-item">
<img src="images/project-6.jpg" alt="About page with a profile paragraph and headshot">
<figcaption>About page</figcaption>
</figure>
</div>
</section>
Each image is wrapped in a <figure> with a <figcaption>. This is the correct semantic structure for images that have captions. The alt attribute on each image provides a text description for screen reader users and for browsers that cannot load the image.
CSS — the gallery grid
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 1rem;
}
.gallery-item {
margin: 0;
overflow: hidden;
border-radius: 6px;
background-color: var(--color-surface);
}
.gallery-item img {
width: 100%;
aspect-ratio: 4 / 3;
object-fit: cover;
display: block;
transition: transform 0.3s ease;
}
.gallery-item:hover img {
transform: scale(1.04);
}
.gallery-item figcaption {
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
color: var(--color-text-secondary);
}
The overflow: hidden on .gallery-item combined with transform: scale(1.04) on the image creates a zoom effect when the user hovers. Because the parent clips overflow, the image enlarges inside its container rather than overflowing into neighboring cells.
Writing Good Alt Text for Gallery Images
Gallery images require careful alt text. The alt text should describe what the image shows in a way that is useful to someone who cannot see it.
Comparing Alt Text
From useless to useful
- Bad
alt="screenshot" tells a screen reader user almost nothing about what the image actually shows.
- Better
alt="Home page of the student learning site showing a hero section with the heading Welcome to Your Learning Journey and a row of three lesson cards below it" describes what is visible so the user understands the content of the screenshot.
- DecorativeWhen images are purely decorative and add no information — for example, abstract background textures — use an empty alt attribute,
alt="". This tells screen readers to skip the image entirely.
Never Omit the Attribute
Never omit the alt attribute altogether. An image without any alt attribute is treated as an unlabeled link or image by many screen readers, which is confusing. If an image is decorative, use alt="" — not no attribute at all.
A Gallery With Lightbox-Style Interaction
A more advanced gallery lets users click a thumbnail to see a larger version. This is called a lightbox pattern. Building a full lightbox from scratch goes beyond this chapter, but here is a simple version using a <dialog> element, which is the modern, accessible HTML element for modal content.
HTML — gallery triggers and the lightbox dialog
<!-- Gallery grid -->
<div class="gallery-grid">
<figure class="gallery-item">
<button class="gallery-trigger" aria-label="View larger: Home page screenshot">
<img src="images/project-1.jpg" alt="">
</button>
<figcaption>Home page</figcaption>
</figure>
<!-- more items -->
</div>
<!-- Lightbox dialog -->
<dialog class="lightbox" id="lightbox" aria-label="Image viewer">
<button class="lightbox-close" id="lightbox-close" aria-label="Close image viewer">✕</button>
<img class="lightbox-image" id="lightbox-image" src="" alt="">
<p class="lightbox-caption" id="lightbox-caption"></p>
</dialog>
CSS — the lightbox
.gallery-trigger {
all: unset;
display: block;
cursor: pointer;
width: 100%;
}
.gallery-trigger:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
dialog.lightbox {
background: #000;
border: none;
border-radius: 8px;
padding: 2rem;
max-width: 90vw;
max-height: 90vh;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
dialog.lightbox::backdrop {
background: rgba(0, 0, 0, 0.75);
}
.lightbox-image {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
}
.lightbox-caption {
color: #fff;
font-size: 0.9375rem;
margin: 0;
}
.lightbox-close {
align-self: flex-end;
background: none;
border: 1px solid rgba(255, 255, 255, 0.4);
color: #fff;
border-radius: 4px;
padding: 0.25rem 0.75rem;
cursor: pointer;
font-size: 1rem;
}
JavaScript — opening and closing the lightbox
const lightbox = document.getElementById("lightbox");
const lightboxImage = document.getElementById("lightbox-image");
const lightboxCaption = document.getElementById("lightbox-caption");
const lightboxClose = document.getElementById("lightbox-close");
// Open lightbox when a trigger button is clicked
document.querySelectorAll(".gallery-trigger").forEach(function(trigger) {
trigger.addEventListener("click", function() {
const img = trigger.querySelector("img");
const caption = trigger.closest(".gallery-item").querySelector("figcaption");
lightboxImage.src = img.src;
lightboxImage.alt = img.closest(".gallery-item").querySelector("figcaption").textContent;
lightboxCaption.textContent = caption ? caption.textContent : "";
lightbox.showModal();
});
});
// Close when close button is clicked
lightboxClose.addEventListener("click", function() {
lightbox.close();
});
// Close when backdrop is clicked
lightbox.addEventListener("click", function(event) {
if (event.target === lightbox) {
lightbox.close();
}
});
Why the Dialog Element Matters
The <dialog> element with the showModal() method gives you focus trapping, backdrop support, and Escape key closing for free. These are behaviors that are very difficult to implement correctly from scratch. Using the native <dialog> element saves you from a large amount of accessibility work.
Avoid Double Announcements
Notice that the trigger images have empty alt="" attributes. The trigger <button> already has an aria-label that describes the image. Having two descriptions — one on the button, one on the image — would create a confusing double announcement for screen reader users.
Section 27.4
Show and Hide Behavior
Before you can understand accordions and tabs, you need to understand the simpler pattern underneath both of them: show and hide behavior.
Show and hide behavior means: a piece of content is initially hidden. When the user clicks a button or trigger, the content becomes visible. When they click again, it hides.
This pattern appears everywhere on the web. Navigation menus on mobile, dropdown filters, expandable product descriptions, collapsible sidebars, and more all use some form of show and hide.
The Basic Pattern
The HTML contains a button and a content panel. The content panel starts hidden:
HTML — a basic toggle
<div class="toggle-group">
<button class="toggle-button" aria-expanded="false" aria-controls="extra-info">
Show more information
</button>
<div class="toggle-panel" id="extra-info" hidden>
<p>
This is the additional information that was hidden. It becomes visible
when the user clicks the button above.
</p>
</div>
</div>
The hidden attribute on the panel makes it invisible by default. The browser applies display: none to elements with the hidden attribute, which removes them from the visual layout and from the accessibility tree.
The aria-expanded="false" attribute on the button tells screen reader users that this button controls a collapsible section, and that the section is currently collapsed. The aria-controls attribute connects the button to the panel by referencing the panel's id.
JavaScript — toggling the panel
const toggleButton = document.querySelector(".toggle-button");
const togglePanel = document.getElementById("extra-info");
toggleButton.addEventListener("click", function() {
const isExpanded = toggleButton.getAttribute("aria-expanded") === "true";
if (isExpanded) {
togglePanel.hidden = true;
toggleButton.setAttribute("aria-expanded", "false");
toggleButton.textContent = "Show more information";
} else {
togglePanel.hidden = false;
toggleButton.setAttribute("aria-expanded", "true");
toggleButton.textContent = "Hide information";
}
});
The JavaScript reads the current aria-expanded value to decide what state the toggle is in. If it is currently expanded, it hides the panel and sets expanded to false. If it is currently collapsed, it shows the panel and sets expanded to true. The button text updates to reflect the current action.
Using CSS Classes for Show and Hide
An alternative to the hidden attribute is toggling a CSS class. This approach gives you more control over visual transitions:
HTML
<div class="toggle-panel" id="extra-info" aria-hidden="true">
<p>This is the additional information.</p>
</div>
CSS — animated max-height transition
.toggle-panel {
overflow: hidden;
max-height: 0;
transition: max-height 0.3s ease;
}
.toggle-panel.is-open {
max-height: 500px; /* large enough to contain any reasonable content */
}
JavaScript
toggleButton.addEventListener("click", function() {
const isExpanded = toggleButton.getAttribute("aria-expanded") === "true";
togglePanel.classList.toggle("is-open");
toggleButton.setAttribute("aria-expanded", String(!isExpanded));
togglePanel.setAttribute("aria-hidden", String(isExpanded));
});
The max-height transition technique is a common CSS trick for animating content expansion. Setting max-height: 0 with overflow: hidden collapses the panel. Setting max-height to a large value with a transition animates the opening.
A Caution
The max-height trick does not work perfectly for panels whose height is unknown. If the content is taller than your max-height value, it will clip. Use a large enough value to cover any realistic content length.
Choosing Between hidden and CSS Classes
A Practical Rule
Use the hidden attribute when you want a clean, simple toggle with no animation. Use CSS classes when you want animated transitions. Either approach is valid. The most important requirement is that the aria-expanded attribute is always updated so screen reader users know the current state.
Section 27.5
Accordions
An accordion is a list of items where each item has a header and a collapsible content panel. Only some panels are open at a time. Users can click a header to open its panel and collapse it again.
Accordions are common for:
- Frequently asked questions (FAQ sections)
- Product feature details
- Legal terms and conditions
- Course topic outlines
- Help and support pages
The Accordion HTML Structure
HTML — a three-item accordion
<section class="accordion" aria-label="Frequently asked questions">
<h2>Frequently Asked Questions</h2>
<div class="accordion-item">
<h3 class="accordion-heading">
<button class="accordion-trigger" aria-expanded="false" aria-controls="faq-1">
What do I need to start learning web development?
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="accordion-panel" id="faq-1" hidden>
<div class="accordion-content">
<p>
You need a text editor, a web browser, and a folder to save your files.
A free text editor like Visual Studio Code works well. Any modern browser
such as Chrome, Firefox, or Edge will do. No special software is required
to get started.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-heading">
<button class="accordion-trigger" aria-expanded="false" aria-controls="faq-2">
How long does it take to learn HTML and CSS?
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="accordion-panel" id="faq-2" hidden>
<div class="accordion-content">
<p>
Most beginners can learn HTML and basic CSS well enough to build simple pages
within a few weeks of consistent practice. Building responsive, accessible,
and maintainable pages takes longer. This course covers both foundations
and good practices so you learn the right habits from the beginning.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h3 class="accordion-heading">
<button class="accordion-trigger" aria-expanded="false" aria-controls="faq-3">
Do I need to learn JavaScript to build a website?
<span class="accordion-icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="accordion-panel" id="faq-3" hidden>
<div class="accordion-content">
<p>
You can build a complete, useful, attractive website with only HTML and CSS.
JavaScript adds interaction: form validation, dynamic content, expanding menus,
and more. This course introduces all three layers so you understand how they
work together, but HTML and CSS are where the foundation is built.
</p>
</div>
</div>
</div>
</section>
There are several important structural choices here.
The trigger is a <button> inside an <h3>. This means the heading is still a real heading in the document outline and can be navigated to by screen reader users using heading navigation. The button handles the click interaction.
The aria-expanded attribute on each button starts as "false" because all panels are closed by default.
The aria-controls attribute connects each button to its panel via the panel's id.
The icon <span> has aria-hidden="true" because it is purely decorative. The plus or minus sign does not add information to a screen reader user who already hears "collapsed" or "expanded" from aria-expanded.
CSS for the Accordion
CSS
.accordion {
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
.accordion-item {
border-bottom: 1px solid var(--color-border);
}
.accordion-item:last-child {
border-bottom: none;
}
.accordion-heading {
margin: 0;
}
.accordion-trigger {
all: unset;
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
padding: 1.125rem 1.25rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
color: var(--color-text);
background-color: var(--color-surface);
transition: background-color 0.15s ease;
box-sizing: border-box;
}
.accordion-trigger:hover {
background-color: var(--color-surface-hover);
}
.accordion-trigger:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: -3px;
}
.accordion-icon {
font-size: 1.25rem;
font-weight: 400;
transition: transform 0.2s ease;
flex-shrink: 0;
}
.accordion-trigger[aria-expanded="true"] .accordion-icon {
transform: rotate(45deg);
}
.accordion-content {
padding: 0 1.25rem 1.25rem;
background-color: var(--color-background);
}
.accordion-content p {
margin: 0;
line-height: 1.7;
}
The all: unset on .accordion-trigger removes all default button styles so you can style it freely. You must add back the properties you need, including cursor: pointer, which all: unset removes.
The icon rotation uses the aria-expanded="true" attribute as a CSS selector: .accordion-trigger[aria-expanded="true"] .accordion-icon. This means you do not need to add or remove a class to rotate the icon. When the button's aria-expanded attribute changes in JavaScript, the CSS automatically updates the icon's appearance. This is an elegant approach because the visual state is always derived from the accessibility state.
JavaScript for the Accordion
JavaScript — single-open accordion
const accordionTriggers = document.querySelectorAll(".accordion-trigger");
accordionTriggers.forEach(function(trigger) {
trigger.addEventListener("click", function() {
const panelId = trigger.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
const isExpanded = trigger.getAttribute("aria-expanded") === "true";
// Optionally close other open panels (single-open accordion)
accordionTriggers.forEach(function(otherTrigger) {
if (otherTrigger !== trigger) {
const otherId = otherTrigger.getAttribute("aria-controls");
const otherPanel = document.getElementById(otherId);
otherTrigger.setAttribute("aria-expanded", "false");
otherPanel.hidden = true;
}
});
// Toggle this panel
if (isExpanded) {
trigger.setAttribute("aria-expanded", "false");
panel.hidden = true;
} else {
trigger.setAttribute("aria-expanded", "true");
panel.hidden = false;
}
});
});
This code handles a single-open accordion: only one panel can be open at a time. When the user opens a new panel, all other panels close. This is the most common accordion behavior.
If you prefer to allow multiple panels to be open simultaneously, remove the loop that closes other panels. Multi-open accordions are less common but appropriate when users might need to compare content across multiple panels.
Tracing the Accordion Logic
Trace
Clicking the second FAQ button for the first time
- Step 1The click event fires on the second trigger button.
- Step 2The code reads
aria-controls from the button. The value is "faq-2".
- Step 3The code finds the element with
id="faq-2". This is the second panel.
- Step 4The code reads
aria-expanded. The value is "false". So isExpanded is false.
- Step 5The loop runs over all triggers. For the first and third triggers (all others), it sets their
aria-expanded to "false" and sets their panels to hidden = true. Since all were already closed, nothing visually changes.
- Step 6Because
isExpanded is false, the code sets the current trigger's aria-expanded to "true" and sets panel.hidden = false.
- Step 7The panel becomes visible. The icon rotates to a × shape via the CSS rule.
Now if the user clicks the same button again:
isExpanded is now "true". So isExpanded === "true" is true.
- The code sets
aria-expanded back to "false" and panel.hidden = true.
- The panel disappears. The icon rotates back to a + shape.
Sections 27.6 – 27.10
Tabs, Class Toggling, Keyboard Access, and Choosing Patterns Wisely
Tabs · Keyboard Interaction · Accessible Patterns · Choosing Patterns
Section 27.6
Tabs
A tab interface presents several panels of content. Only one panel is visible at a time. A row of tab buttons at the top lets the user switch between panels. Clicking a tab makes its panel visible and hides all other panels.
Tabs are useful when you have related but distinct content that would be too long to show all at once: for example, different lesson topics, different product specifications, or different sections of a guide.
The Tab HTML Structure
The HTML for a tab interface uses role attributes to communicate the pattern to screen readers. Tabs are one of the patterns that has a specific ARIA role structure:
HTML — a three-tab interface
<div class="tabs">
<!-- Tab list: the row of tab buttons -->
<div class="tab-list" role="tablist" aria-label="Lesson topics">
<button
class="tab-button"
role="tab"
id="tab-html"
aria-selected="true"
aria-controls="panel-html"
>
HTML
</button>
<button
class="tab-button"
role="tab"
id="tab-css"
aria-selected="false"
aria-controls="panel-css"
tabindex="-1"
>
CSS
</button>
<button
class="tab-button"
role="tab"
id="tab-js"
aria-selected="false"
aria-controls="panel-js"
tabindex="-1"
>
JavaScript
</button>
</div>
<!-- Tab panels: the content for each tab -->
<div
class="tab-panel"
role="tabpanel"
id="panel-html"
aria-labelledby="tab-html"
>
<h3>HTML: Structure and Meaning</h3>
<p>
HTML is the language that gives a web page its structure and meaning.
You use HTML to mark up headings, paragraphs, links, images, lists,
tables, forms, and all other content on a page.
</p>
</div>
<div
class="tab-panel"
role="tabpanel"
id="panel-css"
aria-labelledby="tab-css"
hidden
>
<h3>CSS: Style and Layout</h3>
<p>
CSS controls the visual presentation of a web page. You use CSS to set
colors, fonts, spacing, layout, responsive design, and visual hierarchy.
</p>
</div>
<div
class="tab-panel"
role="tabpanel"
id="panel-js"
aria-labelledby="tab-js"
hidden
>
<h3>JavaScript: Interaction and Logic</h3>
<p>
JavaScript adds behavior to a web page. You use JavaScript to respond to
user events, update the DOM, validate forms, and build dynamic interfaces.
</p>
</div>
</div>
There is more ARIA here than in the accordion. Here is what each piece does.
| Attribute / Role | Purpose |
role="tablist" | On the button container, tells screen readers this group contains tabs. |
role="tab" | On each button, tells screen readers these are tab controls. |
role="tabpanel" | On each content panel, tells screen readers these are the panels controlled by the tabs. |
aria-selected | "true" on the active tab and "false" on all others; tells screen readers which tab is currently selected. |
aria-controls / aria-labelledby | Connect each tab to its panel and each panel back to its tab. |
tabindex="-1" | Removes inactive tabs from the Tab key order. Only the active tab is focusable with the Tab key. |
You navigate between tabs using the arrow keys. This is the expected keyboard behavior for tabs as defined by the ARIA specification. The active panel has no hidden attribute. All other panels start with hidden.
CSS for Tabs
CSS
.tabs {
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
.tab-list {
display: flex;
background-color: var(--color-surface);
border-bottom: 1px solid var(--color-border);
}
.tab-button {
all: unset;
padding: 0.875rem 1.5rem;
font-size: 0.9375rem;
font-weight: 500;
cursor: pointer;
color: var(--color-text-secondary);
border-bottom: 3px solid transparent;
transition: color 0.15s ease, border-color 0.15s ease;
box-sizing: border-box;
}
.tab-button:hover {
color: var(--color-text);
}
.tab-button[aria-selected="true"] {
color: var(--color-accent);
border-bottom-color: var(--color-accent);
}
.tab-button:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: -3px;
}
.tab-panel {
padding: 1.5rem;
background-color: var(--color-background);
}
.tab-panel h3 {
margin-top: 0;
font-size: 1.125rem;
}
.tab-panel p {
margin: 0;
line-height: 1.7;
}
Like the accordion, the active tab style uses an attribute selector: .tab-button[aria-selected="true"]. The active tab gets a bottom border in the accent color. This is the most common visual indicator for the active tab.
JavaScript for Tabs
Tabs require slightly more JavaScript than accordions because of arrow key navigation:
JavaScript — activating tabs with click and arrow keys
const tabButtons = document.querySelectorAll(".tab-button");
const tabPanels = document.querySelectorAll(".tab-panel");
function activateTab(newTab) {
// Deactivate all tabs
tabButtons.forEach(function(tab) {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
// Hide all panels
tabPanels.forEach(function(panel) {
panel.hidden = true;
});
// Activate the new tab
newTab.setAttribute("aria-selected", "true");
newTab.setAttribute("tabindex", "0");
newTab.focus();
// Show the corresponding panel
const panelId = newTab.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
panel.hidden = false;
}
// Click to activate a tab
tabButtons.forEach(function(tab) {
tab.addEventListener("click", function() {
activateTab(tab);
});
});
// Arrow key navigation within the tab list
tabButtons.forEach(function(tab, index) {
tab.addEventListener("keydown", function(event) {
let newIndex;
if (event.key === "ArrowRight") {
newIndex = (index + 1) % tabButtons.length;
activateTab(tabButtons[newIndex]);
} else if (event.key === "ArrowLeft") {
newIndex = (index - 1 + tabButtons.length) % tabButtons.length;
activateTab(tabButtons[newIndex]);
} else if (event.key === "Home") {
activateTab(tabButtons[0]);
} else if (event.key === "End") {
activateTab(tabButtons[tabButtons.length - 1]);
}
});
});
The activateTab() function is a helper that handles both mouse and keyboard activation. It deactivates all tabs, hides all panels, activates the target tab, and shows its panel.
The arrow key logic uses the % modulo operator to wrap around. If the user presses ArrowRight while on the last tab, (index + 1) % tabButtons.length wraps back to 0, activating the first tab. If the user presses ArrowLeft while on the first tab, (index - 1 + tabButtons.length) % tabButtons.length wraps to the last tab.
Home and End keys jump to the first and last tabs respectively. These are standard keyboard behaviors for the tab pattern.
Section 27.7
Class Toggling
You have seen class toggling throughout this chapter. The .is-open class on accordion panels, the aria-expanded attribute selector for icon rotation, the aria-selected attribute selector for tab styling — all of these depend on JavaScript changing attributes or classes in response to user interaction.
Class toggling means adding or removing a CSS class on an element. JavaScript's classList API makes this easy.
The classList API
JavaScript — classList methods
const element = document.querySelector(".my-element");
// Add a class
element.classList.add("is-active");
// Remove a class
element.classList.remove("is-active");
// Toggle: adds if absent, removes if present
element.classList.toggle("is-active");
// Check if a class is present
if (element.classList.contains("is-active")) {
// do something
}
classList.toggle() is especially useful for simple show/hide patterns because it handles the check automatically.
A Practical Example: Mobile Navigation Toggle
A very common use of class toggling is the mobile navigation menu. On small screens, the navigation is hidden. Clicking a hamburger button reveals it.
HTML
<header class="site-header">
<a href="index.html" class="site-logo">LearnSite</a>
<button class="nav-toggle" aria-expanded="false" aria-controls="main-nav" aria-label="Open navigation menu">
<span class="hamburger-icon" aria-hidden="true">☰</span>
</button>
<nav id="main-nav" class="main-nav" aria-label="Main navigation">
<ul class="nav-list">
<li><a href="index.html">Home</a></li>
<li><a href="about.html">About</a></li>
<li><a href="lessons.html">Lessons</a></li>
<li><a href="resources.html">Resources</a></li>
<li><a href="contact.html">Contact</a></li>
</ul>
</nav>
</header>
CSS
@media (max-width: 768px) {
.main-nav {
display: none;
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--color-surface);
border-top: 1px solid var(--color-border);
padding: 1rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.main-nav.is-open {
display: block;
}
.nav-toggle {
display: block;
}
}
@media (min-width: 769px) {
.nav-toggle {
display: none;
}
}
JavaScript
const navToggle = document.querySelector(".nav-toggle");
const mainNav = document.getElementById("main-nav");
navToggle.addEventListener("click", function() {
const isExpanded = navToggle.getAttribute("aria-expanded") === "true";
mainNav.classList.toggle("is-open");
navToggle.setAttribute("aria-expanded", String(!isExpanded));
});
On small screens, the navigation starts without the is-open class and is hidden. Clicking the toggle button adds the class, making the navigation visible. Clicking again removes the class and hides it. The aria-expanded attribute keeps screen reader users informed of the current state.
Naming Toggle Classes
Use State-Based Names
Use clear, state-based names for toggle classes. Good names include:
is-open / is-closed
is-active / is-inactive
is-visible / is-hidden
is-expanded / is-collapsed
is-selected
Avoid vague names like on, go, show1, or clicked. State-based names communicate the current condition of the element.
Section 27.8
Keyboard Interaction at an Introductory Level
Every interactive pattern you build should work without a mouse. Many users navigate entirely by keyboard: people with motor impairments, power users who prefer keyboard efficiency, and screen reader users who navigate with a keyboard.
You do not need to handle every keyboard edge case in an introductory course, but you should understand the basics.
Tab Key Navigation
The Tab key moves focus forward through focusable elements on the page. Shift + Tab moves focus backward. Focusable elements include links, buttons, form inputs, and any element with tabindex="0".
When you build a button or link, it is automatically keyboard-accessible because the browser handles Tab focus, Enter key activation, and Space key activation for you. Using <button> elements instead of <div> or <span> elements for interactive controls is one of the most important accessibility habits you can develop.
Enter and Space Activation
Buttons can be activated with both Enter and Space. Links can be activated with Enter. This behavior is built into the HTML elements. If you use a <div> as a button and add a click listener, keyboard users pressing Enter or Space will not activate it unless you add extra JavaScript.
This is why using semantic HTML elements for interactive controls is not just a good idea — it is essential for accessibility.
Focus Visibility
When a keyboard user presses Tab and moves focus to a button, the browser shows a focus indicator — typically a colored outline around the button. This indicator is critical. Without it, keyboard users cannot tell where focus is on the page.
Do Not Remove Focus Indicators
CSS sometimes removes focus indicators with outline: none. Do not do this. If you want a more styled focus indicator, use :focus-visible and provide your own clear, visible focus style instead of removing the indicator entirely.
CSS — a custom focus style
.my-button:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: 2px;
}
The :focus-visible pseudo-class applies focus styles only when the browser determines the user is navigating by keyboard, not when they click with a mouse. This means mouse users do not see a focus ring after clicking, while keyboard users always do.
Arrow Key Navigation for Widgets
Some patterns have their own keyboard navigation beyond Tab. The tab interface in Section 27.6 uses arrow keys to move between tabs within the tab list. This is the standard behavior for the ARIA tab pattern.
The reason is usability: if each tab were individually Tab-focusable, a tab interface with ten tabs would require ten Tab presses to move through. Arrow keys allow faster navigation within a widget while Tab moves focus to and from the widget as a whole.
You do not need to implement arrow key navigation for every component in this course. But understanding that some patterns have their own keyboard conventions prepares you for more advanced work later.
Section 27.9
Accessible Interactive Patterns
You have seen accessibility considerations throughout this chapter. This section collects those practices and explains the reasoning behind them.
Use the Right HTML Element
The single most effective accessibility habit is using the correct HTML element for the purpose.
Use <button> for controls that do something on the current page. Use <a> for links that navigate to another page or location. Do not use <div>, <span>, or other non-semantic elements as interactive controls.
Using <button> gives you keyboard focus, Enter and Space activation, and screen reader announcement all automatically.
Write Meaningful Labels
Screen reader users hear the label of every button and link. "Click here" and "Read more" are not useful labels. "Open navigation menu", "Close image viewer", "Show answer to: What is HTML?" — these labels tell the user exactly what the control does.
Use aria-label to provide a label when the visual text is insufficient or when the button contains only an icon. Use aria-labelledby to connect an element to a nearby heading that serves as its label.
Update ARIA Attributes in JavaScript
When you change the state of an interactive component, update the ARIA attributes:
- Toggle
aria-expanded when you open and close accordions, menus, and dialogs
- Toggle
aria-selected when you change the active tab
- Toggle
aria-hidden when you hide content from screen readers
Screen readers read ARIA attribute values as users interact. If you open an accordion but forget to update aria-expanded, screen reader users will hear "collapsed" even though the panel is open.
Do Not Hide Content From Keyboard Users
Using visibility: hidden, display: none, or the hidden attribute hides content from both visual and screen reader users. This is appropriate for panels that are genuinely closed. But do not use these techniques to hide content that keyboard users should still be able to reach — for example, skip navigation links that let keyboard users jump past the header.
A skip navigation link is an anchor link placed before the header that jumps to the main content. It is visually hidden by default but becomes visible when it receives keyboard focus:
HTML
<a href="#main-content" class="skip-link">Skip to main content</a>
CSS
.skip-link {
position: absolute;
transform: translateY(-100%);
transition: transform 0.15s ease;
background: var(--color-accent);
color: #fff;
padding: 0.5rem 1rem;
border-radius: 0 0 4px 0;
font-weight: 600;
z-index: 100;
}
.skip-link:focus {
transform: translateY(0);
}
When a keyboard user presses Tab on page load, the skip link appears. If they press Enter, focus jumps to the main content area, skipping the header and navigation. This is a small addition that makes a significant difference for keyboard and screen reader users.
Test With a Keyboard
A Practical Habit
The most direct way to check keyboard accessibility is to close your mouse or trackpad and navigate your page using only the keyboard. Press Tab to move through focusable elements. Press Enter to activate links and buttons. Press Space to activate buttons. Press Escape to close dialogs. See if you can use every feature of the page.
If you get stuck — if focus disappears or gets trapped — find the cause and fix it. Keyboard testing does not require any special tools, just patience and attention.
Worked Example
Building a Complete FAQ Accordion
Here is a complete, working FAQ accordion you can adapt for your own project.
The HTML
faq.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Frequently Asked Questions – LearnSite</title>
<link rel="stylesheet" href="../css/main.css">
</head>
<body>
<header class="site-header">
<!-- shared header content -->
</header>
<main id="main-content">
<section class="faq-section">
<div class="container">
<h1>Frequently Asked Questions</h1>
<p class="faq-intro">
Find answers to common questions about this course. If you do not see
your question here, use the contact form to ask.
</p>
<div class="accordion" id="faq-accordion">
<div class="accordion-item">
<h2 class="accordion-heading">
<button
class="accordion-trigger"
aria-expanded="false"
aria-controls="faq-1"
id="faq-trigger-1"
>
What do I need to start learning web development?
<span class="accordion-icon" aria-hidden="true"></span>
</button>
</h2>
<div
class="accordion-panel"
id="faq-1"
role="region"
aria-labelledby="faq-trigger-1"
hidden
>
<div class="accordion-content">
<p>
You need a text editor, a web browser, and a folder to save your
files in. Visual Studio Code is a free, widely used text editor.
Any modern browser works. No paid software is required.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-heading">
<button
class="accordion-trigger"
aria-expanded="false"
aria-controls="faq-2"
id="faq-trigger-2"
>
How long does it take to learn HTML and CSS?
<span class="accordion-icon" aria-hidden="true"></span>
</button>
</h2>
<div
class="accordion-panel"
id="faq-2"
role="region"
aria-labelledby="faq-trigger-2"
hidden
>
<div class="accordion-content">
<p>
With consistent practice, most students can write basic HTML and
CSS pages within a few weeks. Building responsive, accessible,
and polished pages takes longer. This course builds those habits
from the beginning.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-heading">
<button
class="accordion-trigger"
aria-expanded="false"
aria-controls="faq-3"
id="faq-trigger-3"
>
Do I need to know JavaScript to build a website?
<span class="accordion-icon" aria-hidden="true"></span>
</button>
</h2>
<div
class="accordion-panel"
id="faq-3"
role="region"
aria-labelledby="faq-trigger-3"
hidden
>
<div class="accordion-content">
<p>
No. HTML and CSS are enough for a complete, professional-looking
website. JavaScript adds interaction: menus that open and close,
form validation, dynamic content updates. This course introduces
all three layers.
</p>
</div>
</div>
</div>
<div class="accordion-item">
<h2 class="accordion-heading">
<button
class="accordion-trigger"
aria-expanded="false"
aria-controls="faq-4"
id="faq-trigger-4"
>
What courses come after this one?
<span class="accordion-icon" aria-hidden="true"></span>
</button>
</h2>
<div
class="accordion-panel"
id="faq-4"
role="region"
aria-labelledby="faq-trigger-4"
hidden
>
<div class="accordion-content">
<p>
After completing this course, students can move to Programming
with Python, JavaScript Programming, Databases, or Full-Stack
Web Development. The pathway depends on your goals and interests.
Talk to your instructor or advisor about the best path for you.
</p>
</div>
</div>
</div>
</div><!-- end .accordion -->
</div><!-- end .container -->
</section>
</main>
<footer class="site-footer">
<!-- shared footer content -->
</footer>
<script src="../js/accordion.js"></script>
</body>
</html>
The CSS (in main.css or a component file)
CSS
/* FAQ section */
.faq-section {
padding: 3rem 0;
}
.faq-intro {
max-width: 60ch;
line-height: 1.7;
margin-bottom: 2rem;
color: var(--color-text-secondary);
}
/* Accordion */
.accordion {
border: 1px solid var(--color-border);
border-radius: 10px;
overflow: hidden;
max-width: 720px;
}
.accordion-item {
border-bottom: 1px solid var(--color-border);
}
.accordion-item:last-child {
border-bottom: none;
}
.accordion-heading {
margin: 0;
font-size: 1rem;
}
.accordion-trigger {
all: unset;
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
width: 100%;
padding: 1.125rem 1.25rem;
font-size: 1rem;
font-weight: 600;
line-height: 1.4;
cursor: pointer;
color: var(--color-text);
background-color: var(--color-surface);
transition: background-color 0.15s ease;
box-sizing: border-box;
}
.accordion-trigger:hover {
background-color: var(--color-surface-hover);
}
.accordion-trigger:focus-visible {
outline: 3px solid var(--color-accent);
outline-offset: -3px;
}
.accordion-icon {
display: inline-block;
width: 20px;
height: 20px;
position: relative;
flex-shrink: 0;
}
.accordion-icon::before,
.accordion-icon::after {
content: "";
position: absolute;
background-color: currentColor;
border-radius: 2px;
transition: transform 0.2s ease, opacity 0.2s ease;
}
/* Horizontal bar (always visible) */
.accordion-icon::before {
width: 14px;
height: 2px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Vertical bar (becomes invisible when open) */
.accordion-icon::after {
width: 2px;
height: 14px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.accordion-trigger[aria-expanded="true"] .accordion-icon::after {
transform: translate(-50%, -50%) rotate(90deg);
opacity: 0;
}
.accordion-content {
padding: 0.25rem 1.25rem 1.25rem;
background-color: var(--color-background);
}
.accordion-content p {
margin: 0;
line-height: 1.75;
color: var(--color-text-secondary);
}
The JavaScript (accordion.js)
accordion.js — manages a single-open accordion
// accordion.js
// Manages a single-open accordion.
(function() {
"use strict";
const accordion = document.getElementById("faq-accordion");
if (!accordion) return;
const triggers = accordion.querySelectorAll(".accordion-trigger");
function closeAll() {
triggers.forEach(function(trigger) {
const panelId = trigger.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
trigger.setAttribute("aria-expanded", "false");
if (panel) panel.hidden = true;
});
}
triggers.forEach(function(trigger) {
trigger.addEventListener("click", function() {
const panelId = trigger.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
const isExpanded = trigger.getAttribute("aria-expanded") === "true";
closeAll();
if (!isExpanded) {
trigger.setAttribute("aria-expanded", "true");
if (panel) panel.hidden = false;
}
});
});
}());
The JavaScript is wrapped in an immediately-invoked function expression (IIFE): (function() { ... }()). This is a common pattern for wrapping a block of JavaScript so its variables do not pollute the global scope. You will learn more about scope and IIFEs in later JavaScript courses, but the important thing to know now is that this wrapper prevents naming conflicts when multiple JavaScript files are used on the same page.
Running Project
Running Project Connection
Your running project now has a Lessons page and a Resources page. This chapter's patterns fit naturally into your site.
Student Learning Site — Next Additions
For your Lessons page: Use a card grid to display each lesson topic. Each card should include a title, a short description of what the lesson covers, and a link to the lesson page. Use semantic <article> elements for the cards and a properly labeled <section> with aria-labelledby for the grid.
For a FAQ section: Add an accordion to your Resources page or create a separate FAQ page. Use at least four questions relevant to web development or to the topic of your learning site. Implement the single-open accordion pattern from the worked example.
For a future update: Consider adding a tab interface to your About page with tabs for different topics: About This Site, About the Author, and How to Use the Site. Or add tabs to your Lessons page to organize lessons by topic.
For your gallery page: Build a CSS grid gallery displaying screenshots of each page of your site. Use <figure> and <figcaption> for each image. Write descriptive alt text for every screenshot.
A Quick Win
When you add these patterns, also add skip navigation to every page. It takes two minutes and immediately improves keyboard accessibility for your entire site.
Chapter 27 — Back Matter
Chapter Summary
User interface patterns are repeatable solutions to common content and interaction problems. They work because users recognize them, developers can build and improve them, and the accessibility community has documented how they should behave.
- CardsCard grids use CSS Grid to arrange self-contained content items. Each card uses Flexbox internally to keep layout consistent regardless of content length.
aspect-ratio and object-fit: cover keep images uniform. Hover effects add interactivity without JavaScript.
- GalleriesImage galleries display collections of images in a browsable grid.
<figure> and <figcaption> provide semantic structure. Alt text describes each image. The <dialog> element provides an accessible lightbox with built-in focus trapping and Escape key support.
- Show/HideShow and hide behavior is the underlying mechanism for many patterns. The
hidden attribute removes content from the visual layout and accessibility tree. aria-expanded communicates open/closed state to screen readers. CSS class toggling allows animated transitions.
- AccordionsAccordions are lists of collapsible panels. Buttons inside heading elements combine semantic heading structure with keyboard-accessible interaction.
aria-expanded and aria-controls connect buttons to their panels. The icon can be styled using the aria-expanded attribute as a CSS selector so visual state always matches accessibility state.
- TabsTabs present multiple content panels with only one visible at a time. The ARIA
role="tablist", role="tab", and role="tabpanel" communicate the pattern to screen readers. aria-selected indicates the active tab. Arrow key navigation moves between tabs within the tab list.
- TogglingClass toggling uses
classList.add(), classList.remove(), and classList.toggle() to change an element's visual state. State-based class names like is-open and is-active communicate intent clearly.
- KeyboardKeyboard interaction is not optional. Using semantic HTML elements, maintaining visible focus indicators with
:focus-visible, updating ARIA attributes in JavaScript, and testing with a keyboard are foundational accessibility practices.
- ChoosingPatterns should be chosen to serve users. Cards for collections of similar items. Accordions for lists of collapsible details. Tabs for parallel sections where only one is needed at a time. Galleries for collections of images. Restraint and appropriateness matter as much as technical skill.
Key Terms
accordion
A UI pattern consisting of a list of items where each item has a collapsible content panel that can be opened and closed by clicking the item's header.
aria-controls
An ARIA attribute that identifies which element is controlled by the current element, by referencing the controlled element's id.
aria-expanded
An ARIA attribute on a button or trigger that indicates whether the panel it controls is currently expanded ("true") or collapsed ("false").
aria-hidden
An ARIA attribute that, when set to "true", hides an element from the accessibility tree so screen readers skip it.
aria-label
An ARIA attribute that provides an accessible name for an element, overriding any visible text content.
aria-labelledby
An ARIA attribute that connects an element to another element whose content provides the accessible name.
aria-selected
An ARIA attribute used in tab interfaces to indicate which tab is currently selected.
card
A self-contained content block that represents one item, typically including a title, description, optional image, and a link or action.
card grid
A layout that arranges multiple cards into a responsive grid using CSS Grid.
classList
A JavaScript property that provides methods (add, remove, toggle, contains) for managing CSS classes on an element.
class toggling
The practice of adding or removing CSS classes on an element with JavaScript to change its visual state.
focus indicator
A visible outline or highlight shown around a focused element when the user is navigating by keyboard.
:focus-visible
A CSS pseudo-class that applies styles only when the browser determines the user is navigating by keyboard, not when clicking with a mouse.
gallery
A UI pattern that displays a collection of images in a browsable layout.
hidden attribute
An HTML attribute that removes an element from the visual layout and the accessibility tree.
lightbox
A UI pattern that displays a larger version of an image in an overlay when the user clicks a thumbnail.
role attribute
An ARIA attribute that defines the purpose of an element for assistive technologies. Common values include tab, tablist, tabpanel, and region.
show and hide behavior
A UI pattern in which a piece of content is hidden by default and revealed when the user clicks a trigger.
skip navigation link
A visually hidden link that appears when focused by keyboard and allows users to jump past navigation to the main content.
tab interface
A UI pattern with a row of labeled tab buttons and corresponding content panels, where only one panel is visible at a time.
UI pattern
A proven, reusable solution to a common user interface problem.
Study each code example and answer the questions that follow it.
Code Reading 1
<button class="accordion-trigger" aria-expanded="false" aria-controls="panel-a">
How does CSS specificity work?
<span class="accordion-icon" aria-hidden="true"></span>
</button>
<div class="accordion-panel" id="panel-a" hidden>
<div class="accordion-content">
<p>CSS specificity determines which rule wins when multiple rules
target the same element. ID selectors have the highest specificity
among basic selectors, followed by class selectors, followed by
type selectors.</p>
</div>
</div>
- What attribute tells a screen reader whether the panel is open or closed?
- What does
aria-controls="panel-a" communicate?
- Why does the
<span> have aria-hidden="true"?
- What does the
hidden attribute on the panel do?
- Is the panel currently open or closed? How can you tell?
Code Reading 2
.accordion-trigger[aria-expanded="true"] .accordion-icon {
transform: rotate(45deg);
}
- This selector uses an attribute selector. When does this rule apply?
- What element does
transform: rotate(45deg) affect?
- Why is this technique better than adding a separate class for the open state?
Code Reading 3
function activateTab(newTab) {
tabButtons.forEach(function(tab) {
tab.setAttribute("aria-selected", "false");
tab.setAttribute("tabindex", "-1");
});
tabPanels.forEach(function(panel) {
panel.hidden = true;
});
newTab.setAttribute("aria-selected", "true");
newTab.setAttribute("tabindex", "0");
newTab.focus();
const panelId = newTab.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
panel.hidden = false;
}
- What does the first
forEach loop do?
- What does the second
forEach loop do?
- After both loops run, what is the state of all tabs and panels?
- What four things does the code do to activate
newTab?
- Why is
newTab.focus() included?
Trace through the following accordion JavaScript to predict the result.
Initial State
Three accordion items, all closed
- ItemsThree accordion items: FAQ 1, FAQ 2, FAQ 3
- TriggersAll triggers have
aria-expanded="false"
- PanelsAll panels have
hidden = true
const triggers = document.querySelectorAll(".accordion-trigger");
triggers.forEach(function(trigger) {
trigger.addEventListener("click", function() {
const panelId = trigger.getAttribute("aria-controls");
const panel = document.getElementById(panelId);
const isExpanded = trigger.getAttribute("aria-expanded") === "true";
triggers.forEach(function(t) {
if (t !== trigger) {
t.setAttribute("aria-expanded", "false");
document.getElementById(t.getAttribute("aria-controls")).hidden = true;
}
});
if (isExpanded) {
trigger.setAttribute("aria-expanded", "false");
panel.hidden = true;
} else {
trigger.setAttribute("aria-expanded", "true");
panel.hidden = false;
}
});
});
Trace these interactions in order and state the result after each:
- The user clicks the FAQ 2 trigger.
- The user then clicks the FAQ 3 trigger.
- The user then clicks the FAQ 3 trigger again.
- What is the final state of all three triggers and panels after step 3?
For each scenario, predict what you would see or experience.
- A card grid uses
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)). The screen is 680 pixels wide. How many columns will the grid have? How wide will each column be approximately?
- An accordion trigger has
aria-expanded="false". The CSS rule is .accordion-trigger[aria-expanded="true"] .accordion-icon { transform: rotate(45deg); }. Is the icon rotated right now?
- An
<img> element has width: 100% and aspect-ratio: 4 / 3. The image's container is 240 pixels wide. What will the image's height be?
- A button has
tabindex="-1". What happens when the user presses Tab and reaches that button?
- A panel element has
hidden set to false in JavaScript using panel.hidden = false. What happens to the panel?
Find and explain the problem in each example.
Debugging 1
<div class="accordion-trigger" aria-expanded="false" aria-controls="panel-1">
What is the box model?
</div>
<div class="accordion-panel" id="panel-1" hidden>
<p>The box model describes how every element is surrounded by padding, border, and margin.</p>
</div>
What is the accessibility problem with using a <div> for the trigger?
Debugging 2
const trigger = document.querySelector(".accordion-trigger");
trigger.addEventListener("click", function() {
const panel = document.getElementById("panel-1");
panel.hidden = !panel.hidden;
});
This code toggles the panel's visibility when the trigger is clicked. What important step is missing?
Debugging 3
.tab-button:focus {
outline: none;
}
Why is this rule problematic for keyboard users?
Debugging 4
<figure class="gallery-item">
<img src="images/home.jpg">
<figcaption>Home page</figcaption>
</figure>
What is missing from this image element? What problem does the missing attribute cause?
Debugging 5
tabButtons.forEach(function(tab, index) {
tab.addEventListener("keydown", function(event) {
if (event.key === "ArrowRight") {
const newIndex = index + 1;
activateTab(tabButtons[newIndex]);
}
});
});
What happens when the user presses ArrowRight while on the last tab? How would you fix this?
- Write the HTML for a card with no image. The card should include a title, a two-sentence description, and a link. Use appropriate classes and semantic elements.
- Write the CSS for
.card:hover that produces a subtle upward movement and an increased box shadow. The transition should take 0.2 seconds and use ease timing.
- Write the HTML for an accordion item with the question "What is responsive design?" and a brief answer. Include all necessary ARIA attributes in their initial state (panel closed).
- Write the JavaScript that handles clicking an accordion trigger and toggles its panel open or closed. Your code should update
aria-expanded and toggle the hidden attribute. Your code does not need to close other panels.
- Write the HTML for a tab list with three tabs: Overview, Details, and Resources. Each tab should have the correct ARIA attributes. The first tab should be selected; the other two should not. Include
tabindex="-1" on inactive tabs.
- Write a CSS rule that makes the skip navigation link visible only when it has keyboard focus. Start with the link positioned off-screen with
transform: translateY(-100%).
Choose one of the two following mini-projects and complete it fully.
Option A: FAQ Accordion
Build a complete FAQ page for your running Student Learning Site
HTML
- Valid HTML5 document, linked to your shared CSS file and an
accordion.js file
- A
<main> element with a page <h1>
- An introductory paragraph directing users to the contact page if they have further questions
- An accordion with at least five questions and answers relevant to your site's topic
- Correct ARIA attributes on all triggers and panels; buttons inside heading elements
- All panels start closed
CSS
- The accordion has a border and subtle background colors
- The trigger button shows a visual hover state
- The trigger button shows a clear focus indicator using
:focus-visible
- The icon uses a CSS-only plus-to-cross or plus-to-minus transformation driven by
aria-expanded
JavaScript
- Single-open behavior: opening one panel closes all others
aria-expanded is updated correctly on every click
- No console errors
Accessibility
- Keyboard operable: Tab to triggers, Enter or Space to open/close
- Focus indicator visible when navigating by keyboard
- Screen reader test: open and close panels and listen for state announcements
Option B: Tabbed Lesson Guide
Build a tabbed topic overview page for your Lessons page or a standalone guide page
HTML
- Valid HTML5 document, linked to your shared CSS file and a
tabs.js file
- A
<main> element with a page <h1>
- A tab interface with at least four tabs covering different lesson topics
- Correct
role="tablist", role="tab", and role="tabpanel" attributes
aria-selected, aria-controls, aria-labelledby, and tabindex attributes on the correct elements
- First tab selected and its panel visible on page load; all other panels start hidden
CSS
- Clear active tab indicator (border, background, or color change) driven by
aria-selected
- Hover state on inactive tabs
- Focus indicator on tabs using
:focus-visible
- Panel content has appropriate padding
JavaScript
- Clicking a tab activates it and shows its panel
- Arrow key navigation between tabs
activateTab() helper function used for both click and keyboard activation
aria-selected and tabindex updated on every state change
- No console errors
Accessibility
- Tab key moves focus to and from the tab list
- Arrow keys navigate between tabs
- Enter activates a tab when focused
For each item, describe what you would check and how.
- You have built a card grid. How do you check that each card link has a meaningful accessible name?
- You have built an accordion. How do you check that screen reader users hear the correct expanded/collapsed state when they navigate to each trigger?
- You have built a gallery. Open the browser developer tools and inspect one of the images. What attribute do you look for, and what makes a good value for it?
- You have built a tab interface. You close your mouse and navigate to the tab list using the keyboard. What key should move focus into the tab list? What keys should move between tabs?
- You have added skip navigation to your page. Describe exactly what should happen from the moment a keyboard user presses Tab on a freshly loaded page.
These questions draw on skills from earlier chapters alongside this chapter.
- (Chapter 15) A card grid uses
display: grid. What layout system would you use inside a card to align its image, body, and link? Explain why.
- (Chapter 18) Your card grid uses colors, spacing, and border radius values. Instead of using literal values directly in the component CSS, what technique from Chapter 18 would you use to keep these consistent with the rest of your site?
- (Chapter 22) Write the JavaScript line that selects all
.gallery-trigger buttons on a page.
- (Chapter 21) Your accordion has five triggers. Rather than adding five separate
addEventListener calls, you add listeners using querySelectorAll and forEach. Write the outer structure of this approach without filling in the handler body.
- (Chapter 9) An accordion about frequently asked questions contains a question followed by an answer. Which HTML element is semantically most appropriate for the outer wrapper of a question-and-answer pair? (Hint: it is not
<section> or <div>.)
- (Chapter 12) Your accordion icon rotates when open using
.accordion-trigger[aria-expanded="true"] .accordion-icon. What type of CSS selector is [aria-expanded="true"]?
- (Chapter 14) When you use
all: unset on a button element, you lose cursor: pointer. You also lose box-sizing. What value of box-sizing do you typically want to add back, and why?
- (Chapter 17) Your card grid uses
repeat(auto-fill, minmax(280px, 1fr)). On a 600-pixel-wide screen, how many cards fit per row? On a 900-pixel-wide screen?
Preview — Chapter 28
Fetching Content and Working with JSON at an Introductory Level
Chapter 27 connected HTML, CSS, and JavaScript into complete interactive components: cards, galleries, accordions, and tabs. Your pages now respond to users in controlled, accessible, maintainable ways.
Chapter 28 introduces data-driven pages. So far, all the content on your pages has been written directly into the HTML. In Chapter 28 you will learn about JSON: a text format for structured data. You will learn how to store content — a list of resources, a list of lessons, a list of team members — in a JSON file, and how to read that data using JavaScript's fetch() function and render it onto the page dynamically.
This is the beginning of a significant shift in how you think about web content. Instead of writing each card manually in HTML, you will write one set of code that builds all the cards from data. Change the data file, and the page updates automatically. Chapter 28 keeps things introductory — no live APIs, no server-side code — but it will give you enough of the pattern to recognize how larger web applications work when you encounter them in later courses.