How To Add Tabs To A Gutenberg Page In WordPress

March 15, 2019

Yesterday, I was working on a client project in WordPress that’s being built using Gutenberg and ACF Blocks. One of the requirements is that the client needs to be able to split up a page into multiple tabs. In the past, this is something that I probably would’ve implemented using a repeater or perhaps a flexible content field. However, doing this in Gutenberg would mean that we’d lose the ability to add blocks to any of the tabs, relying instead on meta fields. Far from ideal!

I had to come up with an alternative solution, and decided to introduce a new Gutenberg block: a Tab block. This block will divide the page into different tabs, and will have one text field, which allows us to give a title to the tab. Any content added after this Tab block but before the next Tab block will be part of the tab.

Step 1: Add a new ACF Block

The implementation of an ACF Block is covered in the article linked above, so feel free to check it out if you’re following along. I created a new block called 'tab', and added a text field named 'my_tab_title' (swap out ‘my’ with your own prefix).

We also need a template part to render the tab. I’ve kept it as simple as possible, only outputting the title field with no additional markup:

 * Block Name: Tab

$title = get_field('my_tab_title');

echo esc_html($title);

With this done, we can now add our brand new Tab field in the editor. However, nothing else has changed: all our blocks will still render underneath one another, with no tabs to be seen anywhere. Let’s fix that!

Step 2: Find the locations of each tab

After adding our Tab block, we need to figure out where on the page the Tab blocks are added. After all, that’s where each new tab starts. To accomplish this, we can use a nifty new WordPress function called parse_blocks. This function takes one parameter—the post content—and gives you an array of all blocks on the page:

$blocks = parse_blocks(get_the_content());

Next up, we need to filter out the tabs:

$tabs = array_filter($blocks, function ($block) {
    return $block['blockName'] === 'acf/tab';

We now have a new variable, $tabs, which contains all the tabs we added on the page. More importantly, since array_filter doesn’t modify array keys, the keys of this array contain the position of each tab.

What this means is that if you add a Tab block as the first, sixth and ninth block on the page, the keys of our $tabs variable will be 0, 5, and 8 (they’re one number lower because arrays are null-indexed).

Armed with this knowledge, we know that the indexes of our Tab blocks in the $blocks array can be found using this:

$indexes = array_keys($tabs);

Step 3: Split our blocks array into tabs

Ok, great, we have the locations of each Tab block. How do we turn this into tabs on our web page?

The answer is splitting our $blocks array up into different arrays, where each ‘sub-array’ contains the tab block and all the blocks that come after it (our tab content). To accomplish this, I’ve written a little helper function:

 * Split an array into multiple arrays, based on an array of indexes.
 * For example, passing in [1, 2, 3, 4, 5] as the array and [1, 4] as the
 * indexes gives [[1], [2, 3, 4], [5]].
 * @param array $array The array to be split up.
 * @param array $indexes Array of indexes where to split.
 * @return array
function my_split_array_by_indexes($array, $indexes) {
    $indexes = array_values(array_unique(array_merge([0], $indexes)));

    return array_map(function ($i) use ($indexes, $array) {
        $length = ($i + 1) !== count($indexes) ? ($indexes[$i + 1] - $indexes[$i]) : null;

        return array_slice($array, $indexes[$i], $length);
    }, array_keys($indexes));

For the sake of brevity I won’t dwell on the specifics of this function too much1, but the gist of it is this: it takes two parameters, $array and $indexes, and will return an array of arrays, based on the numbers in the $indexes array. For example, passing in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] as the array and [0, 2, 4, 8] as the indexes gives the following result:

    [1, 2],
    [3, 4],
    [5, 6, 7, 8],
    [9, 10]

This result is of course hypothetical, but we can use this same function to get an array of arrays where each sub-array contains a tab block and the content of this tab. We can put this together in a neat function:

 * Return all blocks in content split by Tab blocks.
 * @param string $content
 * @return array
function my_get_tabs($content) {
    $blocks = parse_blocks($content);

    $headings = array_filter($blocks, function ($block) {
        return $block['blockName'] === 'acf/tab';

    return my_split_array_by_indexes($blocks, array_keys($headings));

Step 4: Render our tabs on the page

Now that we have the content of each tab, it’s time to render our tabs on the page! As far as libraries go, I really love Tabby by Chris Ferdinandi. It’s lightweight, accessible, and doesn’t have any external dependencies. Of course, this is just my preference, and you’re more than welcome to use any library you prefer or even build your own implementation.

In this example, I will be using Tabby to implement our tabs. I will also be using another new WordPress function, render_block, which takes a parsed block object and renders the corresponding HTML.

$tabs = my_get_tabs(get_the_content());

<ul data-tabs>
    <?php foreach($tabs as $i => $tab) : ?>
            <a href="#tab-<?php echo esc_attr($i); ?>">
                <?php echo render_block($tab[0]); ?>
    <?php endforeach; ?>

<?php foreach($tabs as $i => $tab) : ?>
    <div id="tab-<?php echo esc_attr($i); ?>">
        <?php foreach($tab as $block) : ?>
            <?php echo render_block($block); ?>
        <?php endforeach; ?>
<?php endforeach; ?>

Make sure to load Tabby (see the GitHub page linked above for more info), and initialize the script:

    var tabs = new Tabby('[data-tabs]');

And that’s it! We now have an easy way to split up a Gutenberg page into multiple tabs. I hope this is helpful and feel free to message me if you have any questions!

  1. If you have any questions about it, I’m happy to answer them on Twitter. And if you’re wondering what’s going on on the first line, I’m using array_merge to prepend a 0 to the list of indexes, array_unique to prevent any duplicates and finally array_values to reset the keys.
Do you have any questions or comments about this post? Tweet me at @danielpost! I am also available for hire!