When viewing a product in WooCommerce information about the product is displayed in tabs. These tabs are fixed and generated by WooCommerce, outside your control. This post will show you how add code that allow the authors to add custom tabs with custom content to products.

Disclaimer: There is a WooCommerce extension called WooCommerce Tab Manager which provides this feature. It’s not free, though. I have not tested it myself, but as far as I can see it only supports a WYSIWYG (what you see is what you get) editor for the tab contents. This post is for you who want more finetuning of the tab contents or want to write the code yourself without paying for another plugin.

We will use the plugin Advanced Custom Fields (ACF) to simplify the process. But you should be able to quite easily replace ACF’s part with your own custom code if you don’t wish to use the plugin. ACF comes in a free version and a Pro version. In ACF Pro there’s a nifty field type: the repeater, which is perfect for this kind of use. If you however don’t have or wish to purchase a Pro license, don’t worry. The code tutorial below will show you how to add fields using both the free version – and using the repeater in Pro version.

If you are not already familiar with ACF, what ACF helps us with is easy setup of post meta fields – of all kinds. You can easily add an editor, a file picker, date- or colorpicker, a post or category selector with support for multiple choices and reordering, and more. We can achieve the same without ACF but then we would need to code the display and saving of metaboxes content ourselves.

What we will make

To put it simply we want to allow adding custom tabs to product view. Each tab supports a title that is displayed as tab label, and the content that is displayed when clicking on the tab. The custom tabs really depend on the type of project or needs you have; maybe you need tabs for technical specs, a tab with files (e.g. user manuals and such), additional information, or a custom query that displays related products.

In this tutorial we will keep it simple by adding a WYSIWYG editor for the tab contents. As mentioned above it’s easy to implement other field types (e.g. files or a post query), it’s just a difference in the code you write for displayed the tab contents – which is unrelated to this tutorial.

If you have ACF Pro and want to use the repeater for easily adding multiple tabs; skip over the next section. If you only have the free version of ACF, continue on. The disadvantage of not having the repeater field is that you need to define a fixed number of tabs. So the author cannot make unlimited number of tabs as they can with the repeater. But this free solution will work just fine in webshops where you just want specific (number of) custom tabs.

Tutorial for ACF free version

Adding the ACF fields

Adding a new metabox with fields in it is really easy with ACF. You have two options; use ACF’s UI to set up everything, or add the fields by code. Usually setting the fields up in admin is the way to go. But if you need to ensure the fields exist in multiple WordPress sites (e.g. localhost development, test server and live server) you might benefit adding the fields by code in your theme or plugin.

You need to set up the following:

  • A group that displays when the post type equals WooCommerce products
  • A text input for the tab title
  • Whatever field(s) you want for the tab contents. As an example we’ll add a WYSIWYG editor.
  • (Optional) Additional tab titles and tab contents for as many tabs we want to support.

Please take note to remember the field names as you’ll need to refer to them later. I’ve defined the tab title as tab_title and the WYSIWYG field as tab_contents.

If you’d rather add the fields by code, here’s an example. Add this in your theme’s functions.php or plugin file:

if (class_exists('acf')) {
	add_action('acf/init', function() {
		$fields = [
			[
				'key' => 'field_tab_title',
				'label' => __('Custom tab title', 'txtdomain'),
				'name' => 'tab_title',
				'type' => 'text',
			],
			[
				'key' => 'field_tab_contents',
				'label' => __('Custom tab content', 'txtdomain'),
				'name' => 'tab_contents',
				'type' => 'wysiwyg',
				'tabs' => 'all',
				'toolbar' => 'full',
				'media_upload' => 1,
				'delay' => 0,
			],
		];

		acf_add_local_field_group([
			'key' => 'group_custom_woocommerce_tabs',
			'title' => __('Custom Tabs', 'txtdomain'),
			'fields' => $fields,
			'label_placement' => 'top',
			'menu_order' => 0,
			'style' => 'default',
			'position' => 'normal',
			'location' => [
				[
					[
						'param' => 'post_type',
						'operator' => '==',
						'value' => 'product'
					]
				]
			],
		]);
	});
}

If you want more than just one tab, just add another set of tab title and tab contents after line #19. Just remember to keep name unique.

When we edit a product we should see this metabox appear:

Outputting your custom tabs

WooCommerce allows us to filter woocommerce_product_tabs for manipulating tabs. As argument to this filter you get an array for all tabs. The array consists of arrays for each tab with unique keys. But the array for each tab does not contain the actual output of the tab contents. Instead it expects a callback – a name of a function that WooCommerce will run for outputting the tab contents.

Once inside the function you can use global $post to get access to the current post object, or if you want the product object WooCommerce generates, simply do global $product. We need the post ID in order to fetch the value of our custom fields with ACF’s function get_field(). In the code example below we are only fetching the tab title and checks if this is empty or not. If it’s not, then it adds a new tab to the array. It makes sense to not add tabs where the tab title is empty.

if (class_exists('acf') && class_exists('WooCommerce')) {
	add_filter('woocommerce_product_tabs', function($tabs) {
		global $post, $product;  // Access to the current product or post
		
		$custom_tab_title = get_field('tab_title', $post->ID);

		if (!empty($custom_tab_title)) {
			$tabs['awp-' . sanitize_title($custom_tab_title)] = [
				'title' => $custom_tab_title,
				'callback' => 'awp_custom_woocommerce_tabs',
				'priority' => 10
			];
		}
		return $tabs;
	});

	function awp_custom_woocommerce_tabs($key, $tab) {
		global $post;

		?><h2><?php echo $tab['title']; ?></h2><?php

		$custom_tab_contents = get_field('tab_contents', $post->ID);
		echo $custom_tab_contents;
	}
}

Note that you can use ‘priority‘ to control the tab’s position. For example setting it to 1 will make your tab appear first, before all of WooCommerce’s tabs. Define a function name to the ‘callback‘ element. In line #17 we define the function that WooCommerce will run for outputting the tab content.

These tab callback functions will get two parameters; the key and the array element of all values for the current tab. Inside our callback function we output the tab title again, by referring to the provided $tab array. WooCommerce echoes their tab titles inside a h2 so we just do the same. And then we use get_field() to get the value of the tab contents and simply echo the value. Adjust line #23 to fit whatever kind of field types you’ve added (e.g. post object selector, images, or something else).

Note that I have wrapped everything inside an if-check that checks whether or not both WooCommerce and ACF is activated. This is good practice to prevent your site from breaking.

And that’s it! You have now successfully created code to add custom WooCommerce tabs!

If you want to do this with ACF Pro’s repeater field to support unlimited number of tabs, read on.

Tutorial for ACF Pro and repeater

Adding the ACF fields

Add your group using either ACF’s admin UI or adding them by code in your theme or plugin files. We need to set up the following:

  • A group that displays when the post type equals WooCommerce product
  • A repeater with the following sub fields:
    • A text input for the tab title
    • Whatever field(s) you want for the tab contents.

This is how you would set it up using ACF admin:

Or you can add the group by code like so:

if (class_exists('acf')) {
	add_action('acf/init', function() {
		$fields = [
			[
				'key' => 'field_custom_tabs_repeater',
				'label' => __('Custom tabs', 'txtdomain'),
				'name' => 'custom_tabs_repeater',
				'type' => 'repeater',
				'layout' => 'row',
				'button_label' => __('Add new tab', 'txtdomain'),
				'sub_fields' => [
					[
						'key' => 'field_tab_title',
						'label' => __('Tab title', 'txtdomain'),
						'name' => 'tab_title',
						'type' => 'text',
					],
					[
						'key' => 'field_tab_contents',
						'label' => __('Tab content', 'txtdomain'),
						'name' => 'tab_contents',
						'type' => 'wysiwyg',
						'tabs' => 'all',
						'toolbar' => 'full',
						'media_upload' => 1,
						'delay' => 0,
					],
				],
			],
		];

		acf_add_local_field_group([
			'key' => 'group_custom_woocommerce_tabs',
			'title' => __('Custom Tabs', 'txtdomain'),
			'fields' => $fields,
			'label_placement' => 'top',
			'menu_order' => 0,
			'style' => 'default',
			'position' => 'normal',
			'location' => [
				[
					[
						'param' => 'post_type',
						'operator' => '==',
						'value' => 'product'
					]
				]
			],
		]);
	});
}

Either way you should end up with this metabox when editing products:

Outputting your custom tabs

Outputting your custom tabs is very similar to what we did above in for the free version. We filter woocommerce_product_tabs, but here we loop through each repeated item from the repeater. We need a way to identify each element in the repeater with keys, so we generate a key ourselves by using the loop position and a slug-version of the tab title. In the callback function we extract the loop position from the key and use this to refer to the array for our repeater.

if (class_exists('acf') && class_exists('WooCommerce')) {
	add_filter('woocommerce_product_tabs', function($tabs) {
		global $post, $product;  // Access to the current product or post
		
		$custom_tabs_repeater = get_field('custom_tabs_repeater', $post->ID);

		if (!empty($custom_tabs_repeater)) {
			$counter = 0;
			$start_at_priority = 10;
			foreach ($custom_tabs_repeater as $custom_tab) {
				$tab_id = $counter . '_' . sanitize_title($custom_tab['tab_title']);
				
				$tabs[$tab_id] = [
					'title' => $custom_tab['tab_title'],
					'callback' => 'awp_custom_woocommerce_tabs',
					'priority' => $start_at_priority++
				];
				$counter++;
			}
		}
		return $tabs;
	});

	function awp_custom_woocommerce_tabs($key, $tab) {
		global $post;

		?><h2><?php echo $tab['title']; ?></h2><?php

		$custom_tabs_repeater = get_field('custom_tabs_repeater', $post->ID);
		
		$tab_id = explode('_', $key);
		$tab_id = $tab_id[0];

		echo $custom_tabs_repeater[$tab_id]['tab_contents'];
	}
}

Inside our filter function we fetch the value of the repeater and checks if it’s not empty. We then define a counter variable, starting at 0 (arrays always start with position 0), which we increment with 1 for each element inside the loop (at line #18). In the loop for each repeater item we assign them all to the same callback function. We use WordPress’ function sanitize_title() to convert the tab title into a slug version of it, and appends it to the key.

In our callback function at line #31 - 32 we do some simple string manipulations to extract the counter value (which starts at 0 and increments by 1 for each item). We then simply use this as the index for the repeater array in order to fetch the correct tab content field.

And that’s it! You have now implemented unlimited number of custom tabs to WooCommerce!

Remember that you can replace WYSIWYG with any kind of field. You just need to change how you output the field at line #23.

Conclusion

Writing your own code for adding custom WooCommerce tabs is really easy when you’ve grasped the basic concept of how WooCommerce does it. It doesn’t even require a lot of code. This is a perfectly good solution for you who don’t want or can’t invest in extension licenses or just need a simple solution for your webshop.