CSS Accordion Generator
Configure your accordion
Choose a preset, customize questions, styling, and behavior, then preview or export your responsive accordion code.
Accordion content
Add up to eight questions or content sections. Edit the plain-text titles and answers, duplicate items, and move them into the order you need.
The export includes real buttons, ARIA attributes, keyboard navigation, focus styles and reduced-motion support. Check the finished component with your content and colors.
Export HTML and CSS
Copy your code, download a standalone HTML page, or get a ZIP with HTML, CSS and a README.
Build Responsive CSS Accordions with Clear Markup and Reliable Behavior
A CSS accordion is a group of headings that reveal or hide associated content, often used for FAQs, product information and help sections. Toolslight's CSS Accordion Generator helps developers, designers and students configure that interface, preview it at different widths, and export editable HTML, CSS and vanilla JavaScript.
Start with Forest, Ocean, Midnight, Sunset, Lavender or Bold, then choose Cards, Grouped or Minimal panels. Cards separate each answer visually, Grouped panels share edges, and Minimal panels use a lighter divider-based design. These styles change presentation; the exported component uses the same button-and-panel architecture.
Choose a preset, enter your questions and answers, adjust the appearance, and select whether one or several panels may stay open. Test the preview with both a pointer and the keyboard before copying the code or downloading a standalone page or ZIP. The visual editor accepts plain text; richer answer markup can be added carefully after export.
Native HTML or a Custom Button Accordion?
There are two useful starting points: the browser's native <details>/<summary> disclosure, or a custom header button paired with a controlled panel. Native HTML is a strong choice when you want simple opening and closing without JavaScript. A custom implementation gives you direct control over grouping, animations, state and application integration, but you must implement its behavior correctly.
Toolslight currently exports the custom button approach with vanilla JavaScript. “CSS accordion” describes the component and its styling; it does not mean this export operates without a script.
| Decision | Native <details> / <summary> | Custom <button> + panel |
|---|---|---|
| Semantic markup | <summary> labels its parent disclosure; open represents its state |
A real button inside an appropriate heading controls an identified content panel |
| State and accessibility | The browser supplies disclosure behavior and state exposure; test actual browser/screen-reader combinations | Synchronize aria-expanded, panel availability and focus; use unique aria-controls references |
| JavaScript | Not required for ordinary toggle behavior | Required for the custom implementation shown here |
| One open at a time | A shared name groups native disclosures in supporting browsers; older browsers may allow several open |
Explicit group logic controls exclusivity and whether the final panel can close |
| Keyboard interaction | Browser-provided summary activation and normal Tab navigation | Native button activation plus deliberate focus management; additional shortcuts are optional |
| Animation | Basic toggling works without animation; advanced intrinsic-size and ::details-content techniques need feature-specific support checks |
Grid-row transitions handle variable-height answers without hard-coded pixel caps |
| Browser compatibility | Basic disclosures work across current major browsers; group naming and advanced animation have separate support histories | Buttons and ARIA are established; Grid interpolation and inert require their own compatibility strategy |
| Styling flexibility | Preserve an identifiable disclosure control when restyling the marker | Full control over button, icon and panel structure; more implementation responsibility |
| Failure behavior | Still opens and closes without a script | Requires progressive enhancement so content stays readable when JavaScript fails |
| Best fit | Simple FAQs and documentation disclosures | Application UI, custom mode policies and tightly controlled interactions |
For a native accordion, put <summary> first and give each independent group its own name. Remove name when multiple answers may remain open. The name is a group identifier, not a label announced to users.
<details name="delivery-help" open>
<summary>When will my order arrive?</summary>
<p>Your estimated delivery date appears at checkout.</p>
</details>
<details name="delivery-help">
<summary>Can I change my delivery address?</summary>
<p>Contact support before dispatch to request a change.</p>
</details>
Do not add redundant role="button" or manually maintained aria-expanded to this native example. Avoid hiding the entire summary control or replacing it with a non-focusable label. A native group can normally have all its items closed; a policy requiring one panel to stay open needs additional logic. See the HTML disclosure specification and MDN's details reference for the native behavior.
Checkbox and radio-input tricks can produce CSS-driven visuals, but they do not automatically provide disclosure semantics, synchronized expanded state or a reliable content-focus model. Prefer native disclosures or a deliberately implemented button pattern.
Smooth Height Transitions Without a Fixed Content Cap
Traditional CSS transitions interpolate compatible numeric values. A transition from height: 0 to height: auto has no ordinary numeric end value, so a conventional height transition does not smoothly interpolate it. Modern CSS can opt into some intrinsic-size transitions with interpolate-size: allow-keywords, but that feature is still listed as limited availability; it should be a progressive enhancement rather than the only mechanism supporting your content. See MDN's interpolate-size reference.
For a custom accordion, a one-row grid can transition from 0fr to 1fr. The direct grid child needs min-height: 0 so its intrinsic minimum does not hold the panel open, and overflow: hidden clips the answer during the transition. Put answer padding inside that child; padding on the outer panel can leave visible space when the track is collapsed.
.accordion-panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 240ms ease;
}
.accordion-panel[data-open="true"] {
grid-template-rows: 1fr;
}
.accordion-panel__clip {
min-height: 0;
overflow: hidden;
}
.accordion-panel__content {
padding: 0 1rem 1rem;
}
@media (prefers-reduced-motion: reduce) {
.accordion-panel {
transition: none;
}
}
Use the structure panel > clip > content. This CSS only controls appearance: clipping does not remove descendants from keyboard navigation or assistive technology. The complete example below synchronizes the visual state with inert, and falls back to hidden when necessary. Grid track animation is supported in current major browsers; older implementations can switch states without a smooth transition. web.dev explains Grid interpolation.
The grid method allows an open answer to grow when its text, images or translated content change. It does not promise to animate every content mutation, and it is not inherently a compositor-only animation: layout still needs to be calculated. Keep motion short and avoid animating many large panels together.
This technique should not be pasted onto a closed native <details> element with the assumption that its closing content will remain renderable. Native closing behavior and advanced selectors such as ::details-content require their own transition strategy and compatibility checks.
Why max-height is an Imperfect Fallback
Transitioning from max-height: 0 to a large fixed cap works only while the content fits below that cap. A long answer can be clipped, particularly after translation, larger text settings or an image loading. A generous cap also distorts timing: with a linear one-second transition, 300px of content and a 1,000px cap, visible opening finishes after about 0.3 seconds; visible closing does not begin until about 0.7 seconds into the reverse transition. Easing changes those timings further.
An instant, fully usable open/close state is a better fallback than a cap that hides part of an answer. If a legacy implementation must animate a measured pixel height, update its measurement when content changes and separate DOM reads from writes to limit unnecessary layout work.
WCAG 2.2 and the ARIA Accordion Pattern
WCAG success criteria describe accessibility outcomes. The WAI-ARIA Authoring Practices Guide provides an informative implementation pattern; following a sample does not certify an entire page against WCAG.
Use a heading that fits the document outline, with the button as its only direct content. An accordion under an H2 section commonly uses H3 headers:
<h3>
<button type="button" id="help-trigger-1"
aria-expanded="true" aria-controls="help-panel-1">
Can I edit my order?
</button>
</h3>
<div id="help-panel-1" role="region" aria-labelledby="help-trigger-1">
<p>Contact support before dispatch to request an edit.</p>
</div>
aria-expanded belongs on the trigger and tracks its actual state. aria-controls identifies its panel. If the panel is a named region, aria-labelledby points back to the trigger. Every ID must be unique across the document. Regions are optional for short answers; too many simultaneously exposed regions make landmark navigation noisy. If an open panel cannot be closed, communicate that with aria-disabled="true" while preserving the header's focusability. These relationships follow the APG accordion pattern.
| Keyboard check | Expected behavior | Status in this implementation |
|---|---|---|
| Enter / Space | Activate the focused header and apply the selected opening policy | Native button behavior; no duplicate key handler |
| Tab / Shift+Tab | Follow normal document order through headers and controls in open answers | Preserve the standard page sequence; exclude closed-answer controls |
| Down / Up arrows | Move to the next / previous header, wrapping at the ends | Optional convenience implemented here; does not toggle a panel |
| Home / End | Move to the first / last header | Optional convenience implemented here |
| Focus during closure | Never leave focus inside content that has become unavailable | Return focus to that answer's header when necessary |
Arrow and Home/End shortcuts are enhancements, not a separate WCAG requirement for every accordion. Keep the standard Tab sequence and native activation working even when a screen reader intercepts navigation keys.
aria-hidden="true" alone is not a focus-management solution. A clipped link can still receive focus, creating a mismatch between what a keyboard user reaches and what is exposed to assistive technology. inert removes the closed subtree from both focus navigation and the accessibility tree; the aria-hidden reference explains why focusable descendants need special care.
Use a clearly visible focus indicator, descriptive questions and a logical heading hierarchy. Keep decorative icons out of the accessible name with aria-hidden="true"; the text should explain the action without relying on a plus sign or color alone.
Complete HTML, CSS and Vanilla JavaScript Example
This three-item accordion supports exclusive and multi-expand modes, an optional requirement to keep one panel open, keyboard focus shortcuts and reduced-motion preferences. It uses no framework, measures no content heights, and leaves all answers readable before JavaScript runs. In that unenhanced state, header buttons are disabled to avoid presenting controls that do nothing; initialization enables them and applies the requested state.
Put the HTML in your page, load the CSS, and load the JavaScript once with defer or after the markup. Keep a responsive viewport declaration in the document head:
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="accordion.css">
<script src="accordion.js" defer></script>
Replace the demonstration text and /delivery/ link with your own content and URL. If you duplicate this instance, change every shipping-faq ID prefix and its associated references to a new prefix.
<section class="tl-faq" id="shipping-faq" aria-labelledby="shipping-faq-title"
data-tl-faq data-exclusive="true" data-allow-all-closed="true"
data-initial-open="1">
<h2 id="shipping-faq-title">Delivery and returns</h2>
<article class="tl-faq__item">
<h3 class="tl-faq__heading">
<button class="tl-faq__trigger" type="button" disabled
id="shipping-faq-trigger-1" aria-expanded="true"
aria-controls="shipping-faq-panel-1">
<span>When will my order arrive?</span>
<span class="tl-faq__icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="tl-faq__panel" id="shipping-faq-panel-1"
role="region" aria-labelledby="shipping-faq-trigger-1">
<div class="tl-faq__clip">
<div class="tl-faq__content">
<p>Delivery dates appear at checkout. Read our
<a href="/delivery/">delivery information</a> for details.</p>
</div>
</div>
</div>
</article>
<article class="tl-faq__item">
<h3 class="tl-faq__heading">
<button class="tl-faq__trigger" type="button" disabled
id="shipping-faq-trigger-2" aria-expanded="true"
aria-controls="shipping-faq-panel-2">
<span>Can I change the delivery address?</span>
<span class="tl-faq__icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="tl-faq__panel" id="shipping-faq-panel-2"
role="region" aria-labelledby="shipping-faq-trigger-2">
<div class="tl-faq__clip">
<div class="tl-faq__content">
<p>Contact support before dispatch to request an address change.</p>
</div>
</div>
</div>
</article>
<article class="tl-faq__item">
<h3 class="tl-faq__heading">
<button class="tl-faq__trigger" type="button" disabled
id="shipping-faq-trigger-3" aria-expanded="true"
aria-controls="shipping-faq-panel-3">
<span>How do returns work?</span>
<span class="tl-faq__icon" aria-hidden="true">+</span>
</button>
</h3>
<div class="tl-faq__panel" id="shipping-faq-panel-3"
role="region" aria-labelledby="shipping-faq-trigger-3">
<div class="tl-faq__clip">
<div class="tl-faq__content">
<p>Keep your order reference and contact support to arrange a return.</p>
</div>
</div>
</div>
</article>
</section>
The open state is the baseline so a failed script does not hide answers. The script adds data-open="false" to the panels that should close and sets data-ready="true" after initial layout so startup does not animate every panel.
.tl-faq,
.tl-faq * {
box-sizing: border-box;
}
.tl-faq {
--faq-ink: #17211c;
--faq-surface: #fff;
--faq-accent: #066839;
--faq-border: #b3c4ba;
inline-size: 100%;
max-inline-size: 48rem;
min-inline-size: 0;
margin-inline: auto;
color: var(--faq-ink);
font: 1rem/1.6 system-ui, sans-serif;
overflow-wrap: anywhere;
}
.tl-faq__item {
margin-block: .75rem;
border: 1px solid var(--faq-border);
border-radius: .75rem;
background: var(--faq-surface);
}
.tl-faq__heading {
margin: 0;
font: inherit;
}
.tl-faq__trigger {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
inline-size: 100%;
min-inline-size: 44px;
min-block-size: 44px;
padding: 1rem;
border: 0;
border-radius: .65rem;
background: transparent;
color: inherit;
font: inherit;
font-weight: 650;
text-align: start;
cursor: pointer;
}
.tl-faq__trigger:disabled {
opacity: 1;
cursor: default;
}
.tl-faq__trigger:hover:not(:disabled) {
background: #edf7f1;
}
.tl-faq__trigger:focus-visible,
.tl-faq a:focus-visible {
outline: 3px solid var(--faq-accent);
outline-offset: -3px;
}
.tl-faq a {
color: var(--faq-accent);
text-decoration: underline;
}
.tl-faq__icon {
flex: 0 0 auto;
font-size: 1.5rem;
line-height: 1;
}
.tl-faq__trigger[aria-expanded="true"] .tl-faq__icon {
transform: rotate(45deg);
}
/* All answers are readable until JavaScript enhances the component. */
.tl-faq__panel {
display: grid;
grid-template-rows: 1fr;
}
.tl-faq__panel[data-open="false"] {
grid-template-rows: 0fr;
}
.tl-faq__clip {
min-height: 0;
overflow: hidden;
}
/* Padding belongs inside the clipped grid child. */
.tl-faq__content {
padding: 0 1rem 1rem;
}
.tl-faq__content > :first-child { margin-block-start: 0; }
.tl-faq__content > :last-child { margin-block-end: 0; }
.tl-faq__content img { max-inline-size: 100%; block-size: auto; }
.tl-faq[data-ready="true"] .tl-faq__panel {
transition: grid-template-rows 240ms ease;
}
.tl-faq[data-ready="true"] .tl-faq__icon {
transition: transform 240ms ease;
}
/* Instant, accessible fallback when the browser has no inert support. */
.tl-faq__panel[hidden] { display: none; }
@media (max-width: 30rem) {
.tl-faq__trigger { padding: .875rem; }
.tl-faq__content { padding-inline: .875rem; }
}
@media (prefers-reduced-motion: reduce) {
.tl-faq[data-ready="true"] .tl-faq__panel,
.tl-faq[data-ready="true"] .tl-faq__icon {
transition: none;
}
}
The script finds each panel inside its own item rather than relying on a global ID lookup. Unique IDs are still required for valid ARIA relationships. inert handles closed content in supporting browsers; the fallback sets hidden, trading animation for reliable content availability.
(() => {
function enhanceAccordion(root) {
if (root.dataset.ready === 'true') return;
const items = Array.from(root.children)
.filter(item => item.classList.contains('tl-faq__item'))
.map(item => ({
button: item.querySelector('.tl-faq__trigger'),
panel: item.querySelector('.tl-faq__panel')
}));
if (!items.length || items.some(({ button, panel }) => !button || !panel)) return;
const exclusive = root.dataset.exclusive !== 'false';
const allowAllClosed = root.dataset.allowAllClosed !== 'false';
const supportsInert = 'inert' in HTMLElement.prototype;
const isOpen = ({ button }) => button.getAttribute('aria-expanded') === 'true';
function setOpen(item, open) {
const { button, panel } = item;
if (!open && panel.contains(document.activeElement)) button.focus();
button.setAttribute('aria-expanded', String(open));
panel.dataset.open = String(open);
if (supportsInert) {
panel.inert = !open;
} else {
panel.hidden = !open;
}
}
function updateDisabledState() {
const onlyOneOpen = items.filter(isOpen).length === 1;
for (const item of items) {
if (!allowAllClosed && onlyOneOpen && isOpen(item)) {
item.button.setAttribute('aria-disabled', 'true');
} else {
item.button.removeAttribute('aria-disabled');
}
}
}
for (const [index, item] of items.entries()) {
item.button.disabled = false;
item.button.addEventListener('click', () => {
const opening = !isOpen(item);
if (!opening && !allowAllClosed && items.filter(isOpen).length === 1) return;
if (opening && exclusive) {
items.forEach(other => { if (other !== item) setOpen(other, false); });
}
setOpen(item, opening);
updateDisabledState();
});
item.button.addEventListener('keydown', event => {
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
let next;
switch (event.key) {
case 'ArrowDown': next = (index + 1) % items.length; break;
case 'ArrowUp': next = (index - 1 + items.length) % items.length; break;
case 'Home': next = 0; break;
case 'End': next = items.length - 1; break;
default: return;
}
event.preventDefault();
items[next].button.focus();
});
}
let initial = Number.parseInt(root.dataset.initialOpen || '1', 10) - 1;
if (!Number.isInteger(initial) || initial < 0 || initial >= items.length) initial = -1;
if (!allowAllClosed && initial === -1) initial = 0;
items.forEach((item, index) => setOpen(item, index === initial));
updateDisabledState();
root.getBoundingClientRect();
root.dataset.ready = 'true';
}
function init() {
document.querySelectorAll('[data-tl-faq]').forEach(enhanceAccordion);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();
Choose the Opening Policy
Set these attributes on the root before initialization. They are configuration values read at startup, not a reactive settings API.
data-exclusive |
data-allow-all-closed |
Result |
|---|---|---|
true |
true |
At most one panel open; all may be closed |
true |
false |
Exactly one panel stays open |
false |
true |
Any number may be open, including none |
false |
false |
Several may be open, but the last open panel cannot close |
data-initial-open="1" selects the first answer; 2 and 3 select the corresponding items. Use none to start closed when the policy permits it. If all-closed is disallowed, an invalid or empty initial selection falls back to the first answer. This example deliberately initializes one panel; extend initial-state parsing if you need several specific answers expanded on load.
The initializer ignores an instance already marked ready, preventing duplicate listeners. If your application inserts entirely new accordion markup later, run an equivalent initializer after insertion or use the framework's own component lifecycle. Adding items to an existing initialized instance requires explicit reinitialization logic; this static example does not claim a live editor API.
Mobile Responsiveness and Integration
Touch Targets, Reflow and Content
Use 44 × 44 CSS pixels as the minimum design target for header controls. This is the enhanced target-size value associated with WCAG 2.5.5, a Level AAA criterion. WCAG 2.2's Level AA criterion 2.5.8 uses 24 × 24 CSS pixels, with specified exceptions; do not describe 44 × 44 as its universal AA requirement. See Target Size Minimum and Target Size Enhanced.
Let the accordion fill its container up to a readable maximum width. Allow long questions and URLs to wrap, keep images within the answer width, and avoid fixed answer heights. Check the finished page at 320 CSS pixels, larger text settings, browser zoom and both pointer and keyboard input. A device preview is useful, but the actual theme, content and font determine whether the integrated component fits.
Give normal-sized text at least 4.5:1 contrast, or 3:1 for qualifying large text, and verify essential UI cues against adjacent colors. Test hover, focus and expanded states as well as the resting design. The WCAG contrast guidance explains the text thresholds. Never use a hover-only control on a touch interface.
WordPress Themes and Blocks
Put the HTML in a suitable Custom HTML block, theme template or custom block, and enqueue the stylesheet and behavior file through WordPress. Account permissions, sanitization and security plugins may remove scripts pasted into content, so a separate asset is more reliable than embedding a script inside an answer or post body. Load the script once after its markup or with a suitable deferred strategy, and conditionally enqueue it only where the component is used. See WordPress's asset-loading guide.
Keep styles beneath an accordion class rather than targeting every button, h3 or div on the site. Use unique instance prefixes when a template renders more than one group. Check the integrated page for theme styles that override margins, button appearance, outline or [hidden] behavior. Clear relevant caches after changing generated assets.
React and Vue
Convert the markup to a real component and let framework state own the expanded items, button attributes and panel state. Use React onClick or Vue @click to update that state; do not also attach a second independent vanilla script to the same component. Preserve focus before removing or hiding a panel containing the active element.
In React, generate stable accessibility IDs with useId and derive related trigger/panel IDs from that value. Do not use useId as a substitute for stable list keys. In Vue, use a stable per-instance identifier that matches between server and client, and bind the relationships alongside the component's state; the Vue event-handling guide documents its native event binding. Hydrated markup and initial expanded state should agree between server and client.
Tailwind, Bootstrap and Multiple Designs
The export is plain HTML, CSS and vanilla JavaScript. It requires neither Tailwind nor Bootstrap and is not an installable component package. If you convert styles to utility classes, keep the original semantic relationships, reduced-motion rules, target sizes and hidden-focus behavior. Do not initialize Bootstrap's collapse behavior on panels already controlled by this script.
Load shared behavior once for multiple accordions. Distinct styles can be assigned through root-scoped variables, such as #returns-faq { --faq-accent: #253c78; }, rather than loading competing copies of the same unscoped stylesheet. Recheck contrast after any theme change.
Search Visibility and Useful FAQ Content
Include the questions and answers in the server-rendered HTML whenever practical. Collapsing an answer for users is different from omitting it until a click fetches the text. Google can render JavaScript, but server-rendered content reduces dependencies for users and crawlers; see Google's JavaScript SEO guidance.
Write each question around a real task, answer it directly, and add only the detail needed to solve that task. Accordion markup does not guarantee indexing, higher rankings or a featured snippet. Google ended FAQ rich results in May 2026, so FAQ schema should describe the visible content accurately rather than promise an expanded search result. The change is recorded in Google's Search documentation updates.
Frequently Asked Questions FAQs
Is this CSS accordion generator free?
Yes, you can use the generator and download your accordion without paying or creating an account. Choose a preset, edit your questions and preview the result before copying the code. Download the standalone HTML or ZIP when you are ready to keep a local copy.
Is the generated accordion CSS-only?
No, the current generator exports HTML, CSS and vanilla JavaScript. Include all three parts because the script handles opening, closing, keyboard shortcuts and expanded-state updates. For a no-JavaScript alternative, use native details and summary elements as shown in the developer guide.
Can more than one accordion panel stay open?
Yes, turn off Exclusive expand to allow several panels to remain open. Leave Allow all closed enabled so visitors can close an open panel again. Use Open by default to select the initially expanded item, or choose None to start with every answer closed.
How many FAQ questions can I add?
The visual editor supports up to eight accordion items. Each title accepts up to 120 characters and each answer accepts up to 1,000 characters. If you add more items in the exported HTML, give every new trigger and panel unique IDs and update their ARIA references.
Does the accordion work on mobile screens?
Yes, the generated accordion adapts to its container and adjusts spacing on smaller screens. Use the mobile preview to check long questions, then test the exported component inside your actual page. Keep header controls at least 44 CSS pixels high, allow text to wrap and verify the layout at narrow widths and larger text settings.
Can I use the accordion in WordPress, React or Vue?
Yes, you can integrate the exported code into WordPress or adapt it into a React or Vue component. In WordPress, add the markup through a suitable block or template and enqueue the CSS and JavaScript once. In React or Vue, let component state control expansion and use stable IDs instead of attaching a second independent script to the same markup.
Can I add links, images or HTML inside an answer?
The visual editor accepts plain text and escapes HTML, so pasted tags appear as text. Add links or images to the exported markup in your code editor and keep images within the panel width. Before publishing richer content, prevent controls inside closed panels from receiving focus by using inert or an appropriate hidden-state fallback.
Can I put two generated accordions on one page?
Yes, provided each accordion has unique IDs and correctly scoped styles and behavior. Separate exports reuse default IDs, so rename each instance and update its aria-controls and aria-labelledby references together. Load the behavior script once and scope theme rules separately if the instances need different designs.
Why is my exported accordion not opening?
Missing JavaScript, early initialization or broken trigger-to-panel references can stop an exported accordion from opening. Load the stylesheet and script, run the script after the markup exists or with defer, and check the browser console for errors. Keep the extracted ZIP files together and compare your integration with the standalone HTML download.
Are my questions and accordion designs saved automatically?
No, the generator does not automatically save your questions or design. Copy or download the output before refreshing, resetting or leaving the page. Keep the exported files in your project so you can continue editing the code locally.
How do I animate accordion height without a fixed maximum?
Use a CSS Grid row transition from 0fr to 1fr to animate answers without a fixed height cap. Give the inner grid child min-height: 0 and overflow: hidden, with answer padding inside that clipped child. Pair the visual transition with correct focus handling and a prefers-reduced-motion rule, as demonstrated in the complete example.
Can search engines index accordion content and show FAQ rich results?
Search engines can process crawlable accordion answers, but Google no longer displays FAQ rich results. Render the full questions and answers in the page HTML instead of fetching them only after a click. Keep any FAQ schema synchronized with those visible answers and use Search Console URL Inspection to check the rendered page.
Explore Related Developer Tools
Complement your design workflow with our suite of free front-end web design and CSS generators.
- Build the page around your FAQ section with the responsive CSS Grid and Flexbox layout generator, then place the accordion inside the content container.
- Before exporting a custom theme, check accordion text and focus-color contrast against the normal, hover and expanded backgrounds.
- Use the CSS background pattern generator for the surrounding section, keeping the answer panels readable over the decoration.
- For a custom visual style, generate RGBA colors for borders and shadows and check the resulting colors against the surface beneath them.