Dynamic Accordion in WordPress using Meta Box and Alpine.js

This Pro tutorial provides the steps to implement an accordion using Alpine.js in WordPress that pulls the values of sub fields of a Meta Box group for a dynamic FAQ accordion.

Step 1

Install Meta Box and Meta Box AIO plugins.

Create a new field group having a group-type of field with two sub fields or import this.

In this example, faq group field having question text-type sub field and answer wysiwyg-type field.

Set the field group to appear on your desired post type, in this example: page.

Step 2

Edit your Page(s) and populate the field group.

Step 3

Let’s load Alpine.js and its Collapse plugin.

Edit the Template that applies to your singular post type (all Pages in this example) or if you want to set this up on a specific Page, that Page with Bricks.

If using Bricks, go to Settings → PAGE SETTINGS → CUSTOM CODE and paste the following under “Header Scripts”:

<!-- Alpine Plugins -->
<script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
 
<!-- Alpine Core -->
<script src="//unpkg.com/alpinejs" defer></script>

Paste this in Custom CSS:

.faqs {
	display: flex;
	flex-direction: column;
	gap: 16px;
	max-width: 800px;
	margin: 0 auto;
}

.faq {
	background-color: #ffffff;
	box-shadow:  rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, rgba(0, 0, 0, 0.1) 0px 1px 2px -1px;
	border-radius: 0.5rem;
}

.faq__question {
	font-size: 1.4em;
}

.faq__button {
	padding: 2rem 2.5rem;
	font-weight: 700;
	width: 100%;
	background-color: transparent;
	display: flex;
	justify-content: space-between;
}

body.bricks-is-frontend .faq__button:focus {
	outline: none;
}

.faq__answer {
	padding: 0 2.5rem 2rem 2.5rem;
}

.faq__answer ul {
	padding-left: 20px;
}

Add a Code element where you’d like to show the accordion. Paste this code:

<?php
// Avoid Undefined Function Error when Meta Box is not active.
if ( ! function_exists( 'rwmb_meta' ) ) {
	function rwmb_meta( $key, $args = '', $post_id = null ) {
		return false;
	}
}

// Get the value of faq field and store it in a variable.
$faq = rwmb_meta( 'faq' );

if ( ! empty( $faq ) ) {
	// loop counter
	$index = 1; ?>

	<div x-data="{ active: 1 }" class="faqs">
	<?php foreach ( $faq as $faq_row ) {
		$question = isset( $faq_row['question'] ) ? $faq_row['question'] : '';
		$answer = isset( $faq_row['answer'] ) ? $faq_row['answer'] : ''; ?>

		<div x-data="{
			id: <?php echo $index; ?>,
			get expanded() {
				return this.active === this.id
			},
			set expanded(value) {
				this.active = value ? this.id : null
			},
		}" role="region" class="faq">
			<h2 class="faq__question">
				<button
					x-on:click="expanded = !expanded"
					:aria-expanded="expanded"
					class="faq__button"
				>
					<span><?php echo $question; ?></span>
					<span x-show="expanded" aria-hidden="true" <?php echo 1 !== $index ? 'style="display: none;"' : ''; ?>>&minus;</span>
					<span x-show="!expanded" aria-hidden="true" <?php echo 1 === $index ? 'style="display: none;"' : ''; ?>>&plus;</span>
				</button>
			</h2>

			<div x-show="expanded" x-collapse <?php echo 1 !== $index ? 'style="display: none;"' : ''; ?>>
				<div class="faq__answer"><?php echo $answer; ?></div>
			</div>
		</div>
	<?php $index++; } ?>
	</div>
<?php }
?>

Toggle Execute code on.

If you want to control/change the transition duration, replace

x-collapse

with

x-collapse.duration.1000ms

and change 1000 as needed.

Explanation of the code

In

<div x-data="{ active: 1 }" class="faqs">

we are setting the first row to be active on page load.

We have defined a loop counter (which is set to +1 at the end of the loop) to set id property of each for loop item i.e., the accordion row (question + answer). So the first row has id of 1, the second has 2 and so on.

When expanded is referenced, its value is obtained from the boolean returned by the get expanded() function.

:aria-expanded="expanded"

For the first row, the value of active is 1 since that’s what we are initializing for the div that has faqs class. And the value of expanded is true on page load.

Hence Alpine.js adds aria-expanded attribute and sets its value to true for the first row’s div. Its value becomes false when a different row is active and that happens via

x-on:click="expanded = !expanded"

which tells Alpine to toggle the value of expanded when a row’s button is clicked.

To prevent FOUC (Flash Of Unstyled Content), it is better to set display: none to elements that are/get hidden using JS via CSS. That is done using code like:

<?php echo 1 !== $index ? 'style="display: none;"' : ''; ?>

Live Demo

References

https://alpinejs.dev/component/accordion

https://metabox.io/create-group-of-custom-fields-with-meta-box-group/#step-3-displaying-the-value-of-custom-fields-in-the-group-on-the-frontend