Creating Tabs Block in WordPress Gutenberg with ACF Inner Blocks

Going into nested blocks opens up new horizons

Dupovac Emir

4 Minute Read

The Struggle of Creating Dynamic Tabs in Gutenberg

When working with WordPress and Gutenberg, I’ve always found it challenging to create dynamic and flexible components like tabs, accordions, sliders, and carousels. The issue arises when these components need to contain nested, flexible content that isn’t restricted to predefined fields.

Initially, I tried using ACF repeaters to add tab items dynamically. However, this approach had a major limitation: it forced me to define specific fields inside the repeater, making the content inside the tabs rigid and inflexible. This prevented users from adding rich, nested content like headings, images, and other blocks inside the tabs.

Workarounds That Didn’t Work

To overcome this, I experimented with some workarounds:

  • Injecting tab content through the editor inside a container block and then using jQuery to move that content inside the tab structure. This was more of a quick fix than a sustainable solution.
  • Exploring early implementations of ACF Inner Blocks when support was introduced. Unfortunately, these attempts resulted in content duplication when adding new tabs inside the Tabs block, instead of creating unique, individually editable tabs.

The Right Solution: ACF Blocks with Inner Blocks

The best solution I found was to create two separate ACF blocks:

  1. Tabs Block – This acts as a wrapper for all tab items.
  2. Tab Block – Individual tab items that can only be placed inside the Tabs block.

How It Works

  • The Tab Block is restricted to only be added inside the Tabs block, ensuring a clean and structured hierarchy.
  • The Tab Block allows all Gutenberg blocks inside it, making it truly dynamic and flexible.
  • This same approach can be applied to other UI components like accordions, sliders, and carousels, giving developers full control over their structure.

Unfortunately you still have to structure Tabs block as a repeater in order to create tab navigation and link content with navigation. I’m hoping that It will possible to overcome that with feature updates.

Creating ACF Blocks

To keep this post focused on the concept rather than implementation details, I won’t go into the step-by-step process of creating ACF blocks. However, you can refer to the official ACF guide for that: Create Your First ACF Block.

Creating tabs block:

// Register Tabs block
acf_register_block_type(array(
	'name'              => 'tabs',
	'title'             => __('Tabs', 'dupovacemir'),
	'description'       => __('Tabs component with ability to add inner content.', 'dupovacemir'),
	'render_template'   => 'blocks/tabs.php',
	'category'          => 'custom-blocks',
	'icon'              => 'welcome-widgets-menus',
	'supports'          => array(
		'align' => true,
		'mode' => false,
		'jsx' => true,
	),
	'enqueue_assets'    => function(){
		wp_enqueue_style( 'tabs', get_template_directory_uri() . '/blocks/css/tabs.css', get_theme_version(), true );
		wp_enqueue_script( 'tabs', get_template_directory_uri() . '/blocks/js/tabs.js', array('jquery', 'foundation'), get_theme_version(), true);
	},
	'keywords'          => array( 'tabs' ),
	'allowed_blocks'    => [ "acf/tab" ],
));

// Register Tab block

acf_register_block_type(array(
	'name'              => 'tab',
	'title'             => __('Tab', 'dupovacemir'),
	'description'       => __('Only to be used inside of tabs block.', 'dupovacemir'),
	'render_template'   => 'blocks/tab.php',
	'category'          => 'custom-blocks',
	'icon'              => 'align-pull-right',
	'supports'			=> array(
		'align' => true,
		'mode' => false,
                'jsx' => true,
	),
	'enqueue_assets' 	=> function(){
		wp_enqueue_style( 'tab', get_template_directory_uri() . '/blocks/css/tab.css', get_theme_version(), true );
		wp_enqueue_script( 'tab', get_template_directory_uri() . '/blocks/js/tab.js', array('jquery', 'foundation'), get_theme_version(), true);
	},
	'keywords'          => array( 'tabs' ),
	'parent'            => [ 'acf/tabs' ],
));

Tabs block template:

<?php

global $post;

// Create id attribute allowing for custom "anchor" value.
$id = 'tabs-' . $block['id'];
if( !empty($block['anchor']) ) {
	$id = $block['anchor'];
}

// Create class attribute allowing for custom "className" and "align" values.
$className = 'tabs';
if( !empty($block['className']) ) {
	$className .= ' ' . $block['className'];
}
if( !empty($block['align']) ) {
	$className .= ' align' . $block['align'];
}

?>

<section id="<?php echo esc_attr($id); ?>" class="<?php echo esc_attr($className); ?>">

	<ul class="tabs-wrapper" data-tabs id="tabs-unique-<?php echo $id; ?>">
		<?php if(have_rows('tabs')) : ?>
			<?php while(have_rows('tabs')) : the_row(); ?>
		
			<li class="tabs-title <?php echo get_row_index() == 1 ? 'is-active' : ''; ?>">
				<a href="#tab-content-<?php the_sub_field('tab_id'); ?>">
					<p class="title"><?php the_sub_field('tab_title'); ?></p>
				</a>
			</li>
		
			<?php endwhile; ?>
		<?php endif; ?>
	</ul>

	<div class="tabs-content" data-tabs-content="tabs-unique-<?php echo $id; ?>">

		<InnerBlocks/>	

	</div>

</section>

Tab block template:

<?php

global $post;

// Create id attribute allowing for custom "anchor" value.
$id = 'single-tab-' . $block['id'];
if( !empty($block['anchor']) ) {
	$id = $block['anchor'];
}

// Create class attribute allowing for custom "className" and "align" values.
$className = 'single-tab';
if( !empty($block['className']) ) {
	$className .= ' ' . $block['className'];
}
if( !empty($block['align']) ) {
	$className .= ' align' . $block['align'];
}

?>

<div class="tabs-panel" id="tab-content-<?php echo get_field('tab_id'); ?>">
	<InnerBlocks/>
</div>

Conclusion

By splitting the Tabs component into two separate ACF blocks, we achieve a fully dynamic, nested, and flexible solution that integrates seamlessly within Gutenberg. This method ensures better usability, cleaner code, and avoids content duplication issues. The same principle can be applied to other UI components, making this approach highly reusable.

If you’ve struggled with dynamic tab structures in Gutenberg, give this method a try, and let me know your thoughts!