Lorem Ipsum is simply dummy text of the printing and typesetting industry.
It has roots in a piece of classical Latin literature from 45 BC.
It is a long established fact that a reader will be distracted by readable content.
- This is a list item
- This is also a list item
- And this
There are many variations of passages of Lorem Ipsum available.
How it works
The plugin uses a parent faq_accordion
shortcode with nested faq_item
blocks. Each child defines a question (q
attribute) and answer (inner content). PHP parses these into an array, outputs the base HTML markup, and passes the array to JavaScript via wp_localize_script
.
JavaScript then progressively enhances the markup: it injects unique IDs, wires up ARIA attributes for accessibility, and handles open/close behaviour. Optional classes such as first-item-open
or close-on-open
can be passed into the parent shortcode, giving flexible control without additional settings screens.
How it’s built
The accordion is split between PHP (shortcodes + data handling) and JavaScript (progressive enhancement + accessibility wiring).
1. register the shortcodes
On init
, we tell WordPress about our shortcodes. The parent faq_accordion
acts as the container, while each faq_item
represents a single Q&A pair.
// register shortcodes for [faq_accordion] and [faq_item] on WordPress initiation
add_action('init', function () {
add_shortcode('faq_accordion', 'faq_accordion_render_shortcode');
add_shortcode('faq_item', 'faq_item_render_shortcode'); // [faq_item q="..."]Answer[/faq_item]
});
2. PARSE IN PHP
Each faq_item
passes its question q
and answer (the content between the tags) up to the parent faq_accordion
. In the child’s callback we collect that data and store it in an array:
function faq_item_render_shortcode($atts, $content = null)
{
$atts = shortcode_atts(['q' => ''], $atts, 'faq_item');
$q = (string) ($atts['q'] ?? '');
$a = (string) ($content ?? '');
// hand this item up to the parent
faq_items_push($q, $a);
//...//
}
The parent shortcode removes unwanted <p>
/ <br>
tags, runs do_shortcode()
to render children, and passes the parsed items to JS via wp_localize_script()
:
wp_localize_script('faq-accordion', 'FAQAccordionData', [
'items' => $items,
'selector' => '.faq-accordion',
'atts' => $atts,
]);
This way the PHP array is available in JS without inline scripts.
3. RENDER MARKUP
PHP outputs a simple base structure:
<div class="faq-accordion__item">
<h3 class="faq-accordion__heading">
<button class="faq-accordion__button">
<span class="faq-accordion__icon"></span>
<span class="faq-accordion__question"><?php echo $q; ?></span>
</button>
</h3>
<section class="faq-accordion__panel" role="region">
<div class="faq-accordion__answer">
<p><?php echo $a; ?></p>
</div>
</section>
</div>
This is semantic, accessible, and works as plain HTML.
4. ENHANCE WITH JAVASCRIPT
When the script runs, it first pulls in the localized PHP array (FAQAccordionData
). If the accordion root element doesn’t exist, the script stops immediately:
// Define the array item data pulled from PHP
const data = window.FAQAccordionData || {
items: [],
selector: ".faq-accordion",
};
const root = document.querySelector(data.selector);
// Ensure root exists, if not, end JS.
if (!root) return;
Next, we create an array of .faq-accordion__item
elements and a debugging cross-check that the number of PHP items matches the DOM items.
// Define the array from PHP
const items = Array.from(root.querySelectorAll(".faq-accordion__item"));
// Check to make sure JS / PHP array lengths match up
if (data.items && data.items.length && data.items.length !== items.length) {
console.warn("FAQ count mismatch", {
php: data.items.length,
dom: items.length,
});
For each accordion item, JavaScript generates unique IDs for the button (qID
) and panel (aID
). These IDs are wired up with:
aria-controls
(button → panel)aria-labelledby
(panel → button)aria-expanded
andhidden
(to manage open/close state)
This ensures accessibility and proper screen reader support.
Optional functionality is controlled via shortcode classes (passed from PHP). These are tokenised and referenced later in the toggle logic.
Finally, the event listener handles open/close. If close-on-open
is active, it closes all other panels before opening the current one.
// Setup additional class behaviours
const tokens = (data.atts.class || "").split(/\s+/).filter(Boolean);
const firstOpen = tokens.includes("first-item-open");
const closeOnOpen = tokens.includes("close-on-open");
// Reference closeOnOpen on event listener for accordion items:
// Toggle behavior for accordion items
root.addEventListener("click", function (e) {
const button = e.target.closest(".faq-accordion__button");
if (!button || !root.contains(button)) return;
const panelEl = document.getElementById(
button.getAttribute("aria-controls")
);
const expanded = button.getAttribute("aria-expanded") === "true";
// If it contains the class 'close-on-open' hide the panel after clicking on another item
if (closeOnOpen && !expanded) {
items.forEach((el) => {
const otherBtn = el.querySelector(".faq-accordion__button");
const otherPanel = el.querySelector(".faq-accordion__panel");
if (otherBtn && otherPanel && otherBtn !== button) {
otherBtn.setAttribute("aria-expanded", "false");
otherBtn.classList.remove("is-open");
otherPanel.hidden = true;
}
});
}
button.setAttribute("aria-expanded", String(!expanded));
button.classList.toggle("is-open", !expanded);
if (panelEl) panelEl.hidden = expanded;
});
5. STYLE WITH CSS
The accordion toggle icon is built entirely with CSS, using a button that contains a .faq-accordion__icon
span. Two pseudo-elements (:before
and :after
) form the horizontal and vertical lines of a plus symbol. When the accordion is opened, the horizontal line fades out and the vertical rotates into a minus, giving a smooth transition between the two states without needing extra markup or images.