Building a Gutenberg Block for Tabbed Content

Disclaimer: I am still new to React and Gutenberg block development, if there is a better way to code this please let me know in the comments. Otherwise, I hope this might help someone who needs to create a custom Gutenberg block for displaying a tab layout.

Tab Block Demo:

This is the content for tab 1

View the source code for this block. If you want to use on your site, download the zip file and install on your wordpress site.

Hello again!

Table of Contents


Basic Concepts

This block utilizes nested blocks with a parent-child InnerBlock pattern. The parent block acts as the content wrapper and it uses a child inner block with a TextControl component for creating the tab labels, and InnerBlock component for adding the tab panel content.

The input from the child block’s TextControl gets saved to the child block’s “tabLabel” attribute and the parent block runs a function that loops through each child block, grabbing each label and saves to its “tabLabelsArray” attribute.

The array is then iterated through to create the tab label list items.The content from the child block’s InnerBlocks are added to the tab label’s corresponding content panels.


Block Environment Installation

Follow the directions for the WordPress-supported @wordpress/create-block starter block.


Setup the Parent and Child Blocks Edit Functions

  • The “edit.js” file (in the block’s src folder) is where the edit function for the parent block is located. You will need to add another file to this folder for the child block (I named mine “tab.js”) 
  • For the parent block, in edit.js, import tab.js:
import './tab.js';
  • In the block.json file, create an attribute for the parent block to store the tab labels array.
"attributes":{
       "tabLabelsArray":{
           "type": "array",
           "default": []
       },
  • For the child block, in tab.js, register the child block with ‘parent’ key set to whatever you named the parent block (in my case, ‘create-block/tabs’) and add the ‘tabLabel’ attribute that will save the tab labels.
registerBlockType( 'create-block/tab', {
 
   title: __( 'Tab' ),
   icon: 'welcome-add-page',
   parent: [ 'create-block/tabs' ],
   category: 'design',
   attributes: {
       tabLabel:{
           type: 'string',
           default: ''
       },
  • In the parent block edit.js, import the InnerBlocks component, and create an ALLOWED_BLOCKS const that defines an array of blocks that are allowed to be added within the parent block. In this case, we only want to allow one block: the child block ‘create-block/tab’.
import { InnerBlocks } from '@wordpress/block-editor';
const ALLOWED_BLOCKS = [ 'create-block/tab' ];
  • In the return function for edit.js, add the InnerBlocks component and set the allowedBlocks property to the ALLOWED_BLOCKS const you created above
return (
       <div { ...useBlockProps() }>
           <h2>Tabbed Layout Block</h2>
               <InnerBlocks
                   allowedBlocks={ ALLOWED_BLOCKS }
               />             
       </div>
  • In the child block (tab.js), import InnerBlocks and TextControl at the top of the file
import { InnerBlocks } from '@wordpress/block-editor';
import { TextControl } from '@wordpress/components';
  • In the return of the edit function of tabs.js, add the TextControl and InnerBlocks components. For the child block, we want to allow all blocks to be added within the InnerBlocks component and do not need to set the allowedBlocks property.
return (
           <div className={ props.className }>
               <h4>Tab Label</h4>
               <TextControl
               className={ "tab-label_input" }
                   placeholder="Add Tab Label"
                   type="text"
               />
               <h4>Tab Content</h4>
               <InnerBlocks
               />
           </div>
       );
  • In tab.js, inside the edit function (and above the return), add “props” to the edit function arguments and assign the attributes and setAttributes const to props.
edit: ( props ) => {
       const {
          attributes: { tabLabel },
           setAttributes
       } = props;
  • Then create an onChange function for saving the TextControl input to the tabLabel attribute using the setAttributes function
const onChangeTabLabel = newTabLabel => {
           setAttributes({ tabLabel: newTabLabel});
       };
  • Add this function to the TextControl onChange property, and add the attributes to the value property. This will trigger the onChange function that saves user input to the tabLabel attribute
<TextControl
     className={ "tab-label_input" }
     value={ tabLabel }
     onChange={onChangeTabLabel}
     placeholder="Add Tab Label"
     type="text"
/>
  • In the parent block, edit.js, add the props and attributes consts
export default function Edit( props ) {
 
   const {
       attributes,
       setAttributes,
   } = props;
   const { tabLabelsArray } = attributes;
  • Set up a function that will create an array of tab labels by accessing the child block’s “tabLabel” attribute.
const buildTabLabelsArray = () =>{
       //function gets child block attributes and saves as an array to parent attributes
       const parentBlockID = props.clientId;
       const { innerBlockCount } = useSelect(select => ({
           innerBlockCount: select('core/block-editor').getBlockCount(parentBlockID)
       }));
 
       var tabLabels = [];
      
       for (let block = 0; block < innerBlockCount; block++) {
           let tabLabel = wp.data.select( 'core/block-editor' ).getBlocks( parentBlockID )[block].attributes.tabLabel;
           tabLabels.push(tabLabel);
       }
  
       return tabLabels;
   }
 
   var labelsArray = buildTabLabelsArray();
  • Here is where things get tricky. You need to save this array to the parent block’s “tabLabelsArray” attribute, but just calling setAttributes in the edit function causes an infinite loop. To avoid this, you need to set up a condition that checks for a change.

    There are many different ways you could check for a change. After countless hours and the sweat and tears of struggling with different options, I decided to compare the array length of the tabLabelsArray vs the labels array that is created by the buildTabLabelsArray() function.

    If the lengths of the two arrays are different, then we know that there was a change in the child block and that will trigger the parent block to save the new array to the tabLabelsArray attribute.
var labelsArray = buildTabLabelsArray();
   var labelLengthChange = labelsArray.length !== tabLabelsArray.length;
  
   if( labelLengthChange ){
       setAttributes ({ tabLabelsArray: labelsArray  });
   }

Setup the Parent and Child Blocks Save Functions

  • The parent block’s save function is located in src/save.js. In this file, import the InnerBlocks and add props and create the attribute const for accessing the tabLabelsArray contents within save() function.
import { InnerBlocks } from '@wordpress/block-editor';
export default function save( props ) {
   const {
       attributes: { tabLabelsArray }
   } = props;

  • In the return of the save function in save.js, set up the markup that will be displayed on the front end. We want to create an unordered list that displays all the tab labels as list items. We will do this by iterating over the tabLabelsArray using the map() function.

    Note that I also included ternary operators to conditionally add an active class and set the aria-selected attributes, this will be useful later when we write the frontend javascript code.
return (
       <div { ...blockProps } >
           <ul className="tab-labels" role="tablist" aria-label="tabbed content">
               {tabLabelsArray.map((label, i) => {
                   return ( <li className={i == 0 ? "tab-label active" : "tab-label"} role="tab" aria-selected={i == 0 ? "true" : "false"} aria-controls={label} tabindex="0">{label}</li>);   
               })}
           </ul>
           <div className="tab-content">
               <InnerBlocks.Content />
           </div>     
       </div>
   );

  • The child block’s save function is in the tabs.js file below the edit function. In the child block’s save function, add the tab-panel wrapper div and include the InnerBlocks.Content component for displaying the InnerBlocks content.
save: ( props ) => {
       const {
           attributes: { tabLabel }
       } = props;
 
       return (
           <div className="tab-panel" role="tabpanel" tabindex="0" aria-labelledby={tabLabel}>
                   <InnerBlocks.Content />
           </div>
       );
   },


Setup the Frontend Javascript

  • Create an “assets” folder in to your block’s root folder and add a file called “frontend.js”
  • Add the javascript code for the tab layout inside the frontend.js file view my frontend.js file here.
  • Enqueue this script in your blocks php file (mine is called tabs.php), as an argument inside the register_block_type function inside the init function:
function create_block_tabs_block_init() {
   register_block_type( __DIR__ , array(
       'render_callback' => function ( $attribs, $content ){
           if( !is_admin() ) {
 
               wp_enqueue_script(
                   'tabs-frontend',
                   plugins_url('assets/frontend.js', __FILE__),
               );
 
               return $content;
          
           }//if frontend
       }//render callback
   ));
 
}
add_action( 'init', 'create_block_tabs_block_init' );


Setup the Frontend and Editor Styles

  • Add the frontend styles for the tabbed layout to the src/styles.scss file:
$orange: #F57814;
$black: #333;
$light-gray: #bababa;
$white: #fff;
 
.wp-block-create-block-tabs {
   .tab{
       &-labels{
           //border-bottom: 1px solid #ccc;
           padding-left: 0;
           .tab-label{
               display: inline-block;
               list-style: none;
               padding: 1em 2em .5em;
               margin: 2.5px;
               cursor: pointer;
               &.active {
                   background-color: $white;
               }
               &.active, &:focus, &:hover{
                   color:  $orange;
                   border-bottom:  $orange 4px solid;
               }
           }
          
       }
       &-content{
           .tab-panel{
               display:none;
               &.active {
                   display:block;
               }
           }
       }
   }

  • Add styles for the editor in src/editor.scss
 $white: #fff;
 $gray: rgb(218, 216, 216);
.block-editor-block-list__block.wp-block.wp-block-create-block-tabs {
   border: 1px dotted  $gray;
   background: $white;
   padding: 1em;
   h2{
       font-size: 1em;
   }
   .wp-block-create-block-tab {
       background-color:  $gray;
       padding: 5px;
       margin-bottom: 1em;
   }
   .block-editor-inner-blocks{
       background-color: $white;
       h4{
           font-size: .75em;
       }
   }
}

And that should be all the pieces to building a basic tabbed content block!


Further Considerations

Ironing Out the Kinks

So the block should work at this point, but there are some quirks. For one, if you add a new tab, rearrange or delete the childblock tabs in the wordpress editor, it will save to the database but when you view the page  on the frontend the changes will not be reflected. You need to back into the editor, save again, then back to the frontend to see the changes. This is obviously very irritating behavior!

I ended up adding additional attributes to the parent and child blocks and functions to check for different changes and trigger attributes to be saved so that the changes are reflected in real time. 

Adding a Side Layout Option

I also added a Toggle Control which allows you to switch between two different layouts: the default where the tab labels are at the top and the other where the tab labels appear on the side of the content.

All of these changes are reflected in the final code for this project in github.

Future Improvements

The WordPress-supported create-block package is still something I’m trying to get used to and better understand. For example, I’m still not sure what the best practice is for setting up a child block for the parent block. I wasn’t sure how to set up a json file for the child block’s registration settings, so I included the registration settings and edit and save functions to one file, tab.js. It works, but I don’t know if there is a recommended way.

I tried to write the frontend.js and html so that the tabbed content is accessible to people using assistive technology. It works ok, but I think it could be better. For example, right now you can use the tab and enter/return keys to navigate through the tabs and content, but it’s hard to go backwards. I don’t know what the best practice is for accessing the tabs using keyboard keypress, so I’ll need to do more research on that.


Resources

These are the resources I leaned on the most while building this block:


Comments

Comments for this post are closed.