An accessible, animated FAQ accordion built for both visitors and search engines. It handles the interactive question/answer UI on the page while quietly generating the structured data search engines look for, so you’re not maintaining two separate systems for the same content.

Use it for support pages, product FAQs, pricing page objections, or anywhere you’ve got a list of questions worth answering upfront. It works equally well with hand-entered content or FAQs pulled dynamically from a CPT, so it fits whether you’re building a one-off page or a repeatable pattern across dozens of posts.

Drop it into any page and it automatically combines every FAQ instance into a single FAQPage JSON-LD schema, so you get rich snippet eligibility in search results without touching a separate SEO plugin field.

Using the Component

Add the component wherever you want a FAQ block, then choose a data source, and optionally give it a title. Each instance you add is completely independent, so a general FAQ section and a product-specific one can sit on the same page with totally different settings, one open by default, one closed, one restricted to one-at-a-time, one allowing multiple open, without stepping on each other.

Props

Prop Type Values / Notes
source Select "static" / "loop"
faqs Repeater Each item has question and answer subfield
faqsLoop Loop Dynamic data loop; items must expose faq_data.question / faq_data.answer via the normalization filter
title.hasTitle Boolean Whether to show the title above the accordion.
title.text String The eyebrow title text, only shown if hasTitle is true.
title.element Select h2 / h3
settings.openFirstItem Boolean First item starts open, no animation
settings.oneAtATime Boolean Opening one item closes others in that instance
classes Class Custom classes added to the component’s root element

settings.openFirstItem and settings.oneAtATime are read fresh every time the component initializes, so if you’re testing different configurations in the Etch editor, toggling either prop updates the live behavior right away without needing a page reload.

Choosing a Source

props.source controls where the FAQ items come from, either "static" or "loop". Both render identically on the frontend and feed into the same schema output, the only difference is where the question and answer text comes from.

Static uses props.faqs, a repeater of { question, answer } items you enter directly on the component. question is plain text, answer is a text field. Rich text formatting on the Answer field requires the Etch Enhancer plugin, without it the field behaves as plain text.

Loop uses props.faqsLoop, a dynamic data loop. Each item is expected to expose item.faq_data.question and item.faq_data.answer, a normalized shape rather than whatever field names your actual post type uses. Since your FAQ fields are probably named something else depending on your post type or field group, you’ll need a small PHP filter to normalize the field names before Etch sees them. This keeps the component itself source-agnostic instead of hardcoding it to one specific post type.

Connecting a dynamic data source

Since the loop mode always reads from item.faq_data.question and item.faq_data.answer, and your FAQ fields are probably named something else depending on your post type or field group, you’ll need a small PHP filter to normalize the field names before Etch sees them. This keeps the component itself source-agnostic instead of hardcoding it to one specific post type.

The example below is just that, an example. Your exact setup will depend on how your source data is structured (ACF fields, a different plugin, custom meta, and so on). The main thing that matters is that however you pull your data, you normalize it into this shape:

PHP

Our example below covers two common setups. The faq post type uses ACF fields, faq_question and faq_answer. The help_article post type instead uses the post’s own title and content, no ACF fields needed, since question pulls from get_the_title() and answer pulls from get_the_content(). Add this to your theme or a code snippets plugin, and swap in the post types and field keys that match your own setup, adding another row to the map for each additional post type you want the loop to support:

PHP

Once that’s in place, point props.faqsLoop at your post type’s loop and the component will pick up faq_data.question and faq_data.answer automatically. If you want to support another post type later, just add another if block for that post type rather than editing the component itself, which keeps it flexible instead of locking it to one source.

SEO/AEO Schema

FAQ schema had a rocky few years. Google scaled back FAQ rich results in 2023 and fully retired the visible dropdown from search in 2026, so the SERP payoff that made FAQ markup popular has mostly disappeared. But the underlying data isn’t useless. Google still parses FAQ structured data to understand pages, just without showing the rich result, and with AI answer engines increasingly pulling from clearly structured Q&A content, well-marked-up FAQ sections matter again even without the visual snippet.

This component handles that structured data for you automatically. Drop it into any page and it combines every FAQ instance into a single FAQPage JSON-LD schema (no separate SEO plugin field needed). Each instance stays independent for keyboard nav and open/close state, but all instances on a page still merge into that one combined schema, built from the data-faq-question / data-faq-answer attributes on each item. It’s only injected on the live frontend, never in the Etch or Gutenberg editorFAQ schema had a rocky few years. Google scaled back FAQ rich results in 2023 and fully retired the visible dropdown from search in 2026, so the SERP payoff that made FAQ markup popular has mostly disappeared. But the underlying data isn’t useless. Google still parses FAQ structured data to understand pages, just without showing the rich result, and with AI answer engines increasingly pulling from clearly structured Q&A content, well-marked-up FAQ sections matter again even without the visual snippet.

This component handles that structured data for you automatically. Drop it into any page and it combines every FAQ instance into a single FAQPage JSON-LD schema, no separate SEO plugin field needed. Each instance stays independent for keyboard nav and open/close state, but all instances on a page still merge into that one combined schema, built from the data-faq-question / data-faq-answer attributes on each item. It’s only injected on the live frontend, never in the Etch or Gutenberg editor.

How to Customize

Passing arguments to your loop

When using the Loop source option, you can pass arguments (like item count or category) into your loop by editing the loop line in the component.

1. Define arguments in your loop

Your loop needs to accept the arguments you want to pass. Give an argument a default with ?? so the component still works when that value is left blank:

PHP

2. Add props for each argument

Open the FAQ component in the Component Editor and add a Text prop for each argument. For example:

Label Key
Item Count itemCount
FAQ Category questionCategory

3. Pass the props into the loop

Find the loop line inside the Loop Source block:

​{#loop props.faqsLoop as item} ​

Add your arguments:

{#loop props.faqsLoop($count: props.itemCount.toInt(), $term_id: props.questionCategory.toInt()) as item} ​

.toInt() converts the Text prop value to a number, which most query arguments expect.

4. Set the values

Select your loop in the Loop prop as normal, then fill in the new props on the component instance.

Filtering by category

The $term_id argument has no sensible “show everything” default, so if your loop includes the tax_query above, set a category on every instance. If you also need an unfiltered FAQ list, create a second loop without the tax_query and select that one instead.

Styling

SN FAQs shares its styling with SN Accordion and SN Advanced Accordion. Each part carries two classes: a shared sn-accordion* class that holds all the CSS, and its own sn-faq* class, which has no styles and exists for the component’s script and for scoping.

Element Shared styling class FAQ class
The outer container .sn-accordion-wrapper .sn-faq-wrapper
The optional title above the accordion .sn-accordion__heading .sn-faq__heading
The accordion itself .sn-accordion .sn-faq
The <div> wrapping all items .sn-accordion__list .sn-faq__list
An individual question/answer pair .sn-accordion__item .sn-faq__item
The heading wrapping the trigger button (h3 by default; set it under Settings → Item Heading Level) .sn-accordion__question .sn-faq__question
The clickable button .sn-accordion__trigger .sn-faq__trigger
The visible question text inside the trigger .sn-accordion__question-text .sn-faq__question-text
The circular icon wrapper next to the question text .sn-accordion__icon-circle .sn-faq__icon-circle
The vertical line of the plus icon, which collapses into a minus on open .sn-accordion__icon-vertical .sn-faq__icon-vertical
The answer panel (<div role="region">, labelled by its question) .sn-accordion__answer .sn-faq__answer
The inner wrapper of the answer content .sn-accordion__answer-inner .sn-faq__answer-inner

Classes prop: any classes added here are appended to the .sn-faq element, so you can add your own instance-specific classes without editing the component.

Fonts: set font changes on .sn-accordion__question-text, not on the trigger. The site’s global button rule overrides font-family on plain buttons, so font styles on the trigger won’t stick.

States: the trigger is a real <button>, and the panel’s closed state uses the hidden attribute rather than just CSS, so you don’t need to write your own show/hide logic. Style the states you care about (hover, focus, aria-expanded="true" on the trigger, and so on) directly off these classes.

Schema: the FAQPage schema is built from the data-faq-question and data-faq-answer attributes on each item, not from the visible markup. Restyling or changing the heading level won’t affect it.

Accessibility

The accordion is built with accessibility first:

Keyboard Navigation

Each instance supports full keyboard navigation on its own, so users can move through triggers with the arrow keys, jump to the first or last item, and the behavior stays scoped to whichever FAQ block they’re currently in.

Key Action
↓ Focus next trigger
↑ Focus previous trigger
Home Focus first trigger
End Focus last trigger

Debugging

If something’s not showing up as expected, open your browser console and look for messages prefixed with FAQ:.

Message Meaning
No [data-faq-component] elements found on this page. The wrapper markup isn’t present, check the HTML actually rendered.
No .sn-faq__trigger elements found. An instance exists but has no items, check props.faqs or props.faqsLoop isn’t empty.
Could not find answer element with id "..." A trigger’s aria-controls doesn’t match any .sn-faq__answer id, usually a slug mismatch.
Skipping item with missing question or answer data. That item won’t appear in the schema, check data-faq-question / data-faq-answer are populated.
No valid FAQ items to build schema from. None of the items had usable data, schema was not injected.

To verify schema on the live site, open DevTools → Elements → search <head> for <script id="sn-faq-schema-ld-json"> and confirm the JSON matches your visible FAQ content.