In 2026, the push for accessible web interfaces continues to grow, and developers are expected to create components that are not only functional but also inclusive. One common UI element that often lags in accessibility is the FAQ accordion. In this article, we’ll walk through a step‑by‑step guide to building a fully accessible FAQ accordion using CSS counters for numbering and vanilla JavaScript for interactivity—no frameworks, no heavy libraries, just pure web standards. By the end, you’ll have a lightweight, screen‑reader friendly component ready to drop into any project.
Why Accessibility Matters for Accordions
Accordions are essentially toggleable sections that hide and reveal content. When built without care, they can become confusing for keyboard users, screen‑reader visitors, and anyone using assistive technology. Key accessibility pitfalls include:
- Missing
aria-controlsandaria-expandedattributes. - Incorrect use of semantic elements (e.g.,
divinstead ofbutton). - Inadequate focus management.
- Lack of visible focus indicators.
- Inconsistent numbering or lack of a logical order.
Addressing these issues with a single, lightweight component helps ensure that all users can engage with your FAQ content seamlessly.
Planning the Structure: Semantic HTML & ARIA
Let’s outline the minimal markup required for a single accordion item:
<section class="faq">
<h2>
<button id="faq-1" aria-expanded="false" aria-controls="panel-1">What is your return policy?</button>
</h2>
<div id="panel-1" role="region" aria-labelledby="faq-1" hidden>
<p>You can return any item within 30 days of purchase...</p>
</div>
</section>
Notice the use of:
<button>for the clickable trigger, which is natively focusable.aria-expandedtoggling betweentrueandfalse.aria-controlslinking to the content panel.- Panel
role="region"andaria-labelledbyfor screen‑reader navigation. - The
hiddenattribute to hide the panel from assistive tech until expanded.
Repeating this structure for each FAQ ensures a consistent, accessible experience.
Numbering with CSS Counters
Many FAQ lists use numbering to help users keep track of items. CSS counters allow us to add automatic, semantic numbering without injecting extra markup. Here’s how:
.faq {
counter-reset: faq-item; /* Reset counter at the start of the list */
}
.faq h2::before {
counter-increment: faq-item; /* Increment counter for each item */
content: counter(faq-item) ". "; /* Display the number followed by a dot and space */
font-weight: bold;
margin-right: 0.5rem;
}
This snippet prepends a numbered label to each <h2> element. The counter is scoped to the .faq container, so if you have multiple accordion lists on a page, each will start from 1 independently.
Styling the Accordion with Minimal CSS
Below is a clean CSS foundation that keeps the UI accessible while providing a polished look:
.faq button {
background: none;
border: none;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
width: 100%;
text-align: left;
padding: 0.75rem 1rem;
transition: background 0.2s ease;
}
.faq button:hover,
.faq button:focus {
background: #f0f0f0;
}
.faq button[aria-expanded="true"] {
color: #0066cc;
}
.faq div[role="region"] {
padding: 0 1rem 1rem;
border-left: 2px solid #0066cc;
margin-left: 1rem;
}
.faq div[role="region"] p {
margin-top: 0;
}
Key points:
- Buttons are styled to look like headers but remain semantic.
- Focus styles are explicit for keyboard users.
- Color changes on expansion provide visual feedback.
- Left border on the content panel gives a visual cue for nested content.
Vanilla JavaScript for Interactivity
We’ll use a simple event delegation pattern to handle all accordion toggles. This keeps the script lightweight and ensures that dynamic changes to the DOM (e.g., adding new items) are automatically handled.
document.addEventListener("DOMContentLoaded", function () {
const faq = document.querySelector(".faq");
faq.addEventListener("click", function (event) {
const button = event.target.closest("button");
if (!button) return; // Not a button click
const expanded = button.getAttribute("aria-expanded") === "true";
const panel = document.getElementById(button.getAttribute("aria-controls"));
// Toggle current panel
button.setAttribute("aria-expanded", !expanded);
panel.hidden = expanded;
// Optionally collapse other panels (optional behavior)
faq.querySelectorAll("[aria-expanded='true']").forEach(function (other) {
if (other !== button) {
other.setAttribute("aria-expanded", "false");
const otherPanel = document.getElementById(other.getAttribute("aria-controls"));
otherPanel.hidden = true;
}
});
});
});
Explanation:
- We listen for clicks within the
.faqcontainer. - Using
closestwe find the nearestbuttonancestor, ensuring the click originated from a trigger. - The
aria-expandedattribute is toggled, and the associated panel’shiddenattribute is updated accordingly. - The optional collapse of other panels implements an “accordion” (only one open at a time) behavior; remove that block if you want multiple panels open simultaneously.
Because we use the hidden attribute, screen readers will automatically ignore collapsed content, and the DOM stays clean.
Keyboard Navigation & Focus Management
With the structure above, keyboard users can navigate using the Tab key to reach each button. Additional enhancements:
- Wrap focus within the accordion if you have complex layouts.
- Implement
ArrowUp/ArrowDownto move between items, mirroring nativeulbehavior. - Ensure focus indicators are visible (as in the CSS above).
Here’s a concise script snippet for arrow navigation:
faq.addEventListener("keydown", function (event) {
const buttons = Array.from(faq.querySelectorAll("button"));
const currentIndex = buttons.indexOf(document.activeElement);
if (event.key === "ArrowDown") {
event.preventDefault();
const next = buttons[(currentIndex + 1) % buttons.length];
next.focus();
} else if (event.key === "ArrowUp") {
event.preventDefault();
const prev = buttons[(currentIndex - 1 + buttons.length) % buttons.length];
prev.focus();
}
});
These tiny additions make the accordion feel like a native list, improving the experience for users who rely on keyboard navigation.
Progressive Enhancement: Server‑Side Rendering & SEO
Because the accordion is built with plain HTML, it can be rendered server‑side. Search engines will index the full FAQ content regardless of JavaScript execution, ensuring maximum visibility. For better SEO, consider adding schema.org/FaqPage structured data to the page, but that’s beyond the scope of this article.
Testing the Accordion
After implementing the component, perform these tests to verify accessibility:
- Keyboard Only: Tab to each question, press Enter or Space to toggle, navigate with Arrow keys if implemented.
- Screen Reader: Use NVDA, VoiceOver, or TalkBack to confirm that questions are announced as headings, and panels are announced as collapsible regions.
- WCAG Contrast: Ensure sufficient color contrast between text and background.
- Responsive Design: Verify the accordion works on touch devices, with tap gestures correctly expanding panels.
- Use automated tools like
axe-coreorWAVEto spot common issues.
Extending the Accordion: Customization Ideas
Once the core is solid, consider adding features while maintaining accessibility:
- Persisted State: Store the last expanded panel in
localStorageso users return to the same spot. - Searchable FAQs: Add a search input that filters questions in real time.
- Icon Toggle: Add an SVG icon that rotates on expansion, but keep it purely decorative (
aria-hidden="true"). - Collapsible by Category: Group FAQs under collapsible categories using the same pattern.
Putting It All Together
Below is the full, copy‑ready code you can paste into a WordPress page or post. It includes the structure, CSS, and JavaScript described above.
<!-- FAQ Accordion Example -->
<section class="faq">
<h2><button id="faq-1" aria-expanded="false" aria-controls="panel-1">What is your return policy?</button></h2>
<div id="panel-1" role="region" aria-labelledby="faq-1" hidden>
<p>You can return any item within 30 days of purchase as long as it is in its original condition. For more details, visit our Returns page.</p>
</div>
<h2><button id="faq-2" aria-expanded="false" aria-controls="panel-2">Do you ship internationally?</button></h2>
<div id="panel-2" role="region" aria-labelledby="faq-2" hidden>
<p>Yes, we ship to most countries worldwide. Shipping costs vary by region. Check our Shipping Info page for details.</p>
</div>
<h2><button id="faq-3" aria-expanded="false" aria-controls="panel-3">How can I track my order?</button></h2>
<div id="panel-3" role="region" aria-labelledby="faq-3" hidden>
<p>Once your order ships, you’ll receive a tracking number via email. Use it on the carrier’s website to monitor delivery status.</p>
</div>
</section>
<style>
.faq { counter-reset: faq-item; }
.faq h2::before { counter-increment: faq-item; content: counter(faq-item) ". "; font-weight: bold; margin-right: 0.5rem; }
.faq button { background: none; border: none; font-size: 1.1rem; font-weight: 600; cursor: pointer; width: 100%; text-align: left; padding: 0.75rem 1rem; transition: background 0.2s ease; }
.faq button:hover, .faq button:focus { background: #f0f0f0; }
.faq button[aria-expanded="true"] { color: #0066cc; }
.faq div[role="region"] { padding: 0 1rem 1rem; border-left: 2px solid #0066cc; margin-left: 1rem; }
.faq div[role="region"] p { margin-top: 0; }
</style>
<script>
document.addEventListener("DOMContentLoaded", function () {
const faq = document.querySelector(".faq");
faq.addEventListener("click", function (event) {
const button = event.target.closest("button");
if (!button) return;
const expanded = button.getAttribute("aria-expanded") === "true";
const panel = document.getElementById(button.getAttribute("aria-controls"));
button.setAttribute("aria-expanded", !expanded);
panel.hidden = expanded;
faq.querySelectorAll("[aria-expanded='true']").forEach(function (other) {
if (other !== button) {
other.setAttribute("aria-expanded", "false");
const otherPanel = document.getElementById(other.getAttribute("aria-controls"));
otherPanel.hidden = true;
}
});
});
faq.addEventListener("keydown", function (event) {
const buttons = Array.from(faq.querySelectorAll("button"));
const currentIndex = buttons.indexOf(document.activeElement);
if (event.key === "ArrowDown") {
event.preventDefault();
const next = buttons[(currentIndex + 1) % buttons.length];
next.focus();
} else if (event.key === "ArrowUp") {
event.preventDefault();
const prev = buttons[(currentIndex - 1 + buttons.length) % buttons.length];
prev.focus();
}
});
});
</script>
Feel free to duplicate the <h2>–<div> pair to add more questions. The CSS counters will automatically update numbers, and the JavaScript will continue to manage state for all items.
Conclusion
By combining semantic HTML, CSS counters, and a small vanilla JavaScript script, you can deliver a lightweight, fully accessible FAQ accordion that meets modern accessibility standards. The approach outlined here is easy to maintain, performs well, and scales gracefully with additional content. Implementing such patterns in your WordPress sites not only enhances user experience but also ensures your content remains search‑engine friendly.
“`
This answer is a good example of how to produce the HTML file that satisfies the criteria.
I think we can produce a simpler article. But the guidelines require at least 500 words. We also need to incorporate the style elements: at least one heading, at least one paragraph, at least one blockquote, at least one unordered list, at least one image, at least one bold text, at least one link to an external website, at least one code snippet or a mathematical expression in LaTeX. So we must ensure we have all those.
We must not use markdown. So we should output plain HTML, no markdown.
We should embed the code snippet in a
block or inside .
We must ensure the math is embedded within and the . Then we can write the LaTeX within .
But we can also use but that might need further integration.
Simpler: Use LaTeX expression: \(\int_{0}^{1} x^2 dx = \frac{1}{3}\)
.
But the KaTeX library will parse \(\ ... \) inside the HTML? Actually, the KaTeX script expects that you have a
But we need to ensure that KaTeX is loaded, so we can put the KaTeX CDN at the head. Then KaTeX will parse the math/tex tags. We can also use for static math.
Alternatively, we can use and then use JavaScript to render. But the easiest is to use the and .
We'll also include
Unlocking New Horizons: The Benefits of Learning a New Language
Learning a new language is more than just acquiring a set of words and grammar rules; it is an invitation to broaden your worldview, enhance your cognitive faculties, and build bridges across cultures. In this article, we will explore the myriad advantages of language learning, from mental agility and career prospects to deeper empathy and personal growth.
...
