Add Additional Tabs to Product Pages in WooCommerce

[September 2023]

A Tutorial with Full Code

Though there are a plethora of plugins available to add tabs to your WooCommerce product pages, the code itself is fairly straightforward to replicate for your needs!

In this example, I go through the steps to add the code to your WordPress functions.php and provide ways to adjust the code to allow for HTML and to be a text editor rather than using the custom meta fields on your WordPress product pages.

Step 1:

The first step is to add custom meta fields using the ‘add_post_meta’ function. This allows us to create custom fields and store data for each product. The following code can be saved in functions.php for your child theme. You will want to add all of the tabs that you would like to display on the product page here.

// Add custom meta field to WooCommerce product
function add_custom_meta_fields() {
    // Replace 'custom_ingredients' and 'custom_resources' with your desired meta field keys
    add_post_meta(get_the_ID(), 'custom_ingredients', '', true);
    add_post_meta(get_the_ID(), 'custom_resources', '', true);
}
add_action('woocommerce_new_product', 'add_custom_meta_fields');
add_action('woocommerce_admin_process_product_object', 'add_custom_meta_fields');

Step 2:

The second step is to create the text fields on the product pages to enter the information that you would like to display for each product. The following code will achieve this result.

By default, WooCommerce sanitizes the content entered in custom meta fields to prevent potential security risks. We will override this behavior in future steps. However, if you are okay accepting the risk, I’ll provide that code in Step 3.

// Callback function to display custom tab content
function custom_product_tab_content() {
    global $post;

    // Get the current tab ID to identify which tab we are on
    $current_tab = sanitize_key($_GET['tab']);

    // Output the "Ingredients" tab content
    if ($current_tab === 'ingredients_tab') {
        $ingredients = get_post_meta($post->ID, 'custom_ingredients', true);
        if (!empty($ingredients)) {
            echo '<h2>' . __('Ingredients', 'woocommerce') . '</h2>';
            echo '<p>' . $ingredients . '</p>';
        }
    }

    // Output the "Educational Resources" tab content
    if ($current_tab === 'educational_resources_tab') {
        $educational_resources = get_post_meta($post->ID, 'custom_resources', true);
        if (!empty($educational_resources)) {
            echo '<h2>' . __('Educational Resources', 'woocommerce') . '</h2>';
            echo '<p>' . $educational_resources . '</p>';
        }
    }
}
The full code that we are utilizing in functions.php is below:

In this example we have tabs for ingredients and educational resources Without HTML allowed in the meta data.

// Add custom meta fields to WooCommerce product data meta box
function add_custom_product_data_fields() {
    woocommerce_wp_textarea_input(
        array(
            'id'          => 'custom_ingredients',
            'label'       => __('Ingredients', 'woocommerce'),
            'placeholder' => __('Enter product ingredients...', 'woocommerce'),
            'desc_tip'    => 'true',
            'description' => __('Enter the ingredients for this product.', 'woocommerce')
        )
    );

    woocommerce_wp_textarea_input(
        array(
            'id'          => 'custom_resources',
            'label'       => __('Educational Resources', 'woocommerce'),
            'placeholder' => __('Enter educational resources...', 'woocommerce'),
            'desc_tip'    => 'true',
            'description' => __('Enter educational resources for this product.', 'woocommerce')
        )
    );
}
add_action('woocommerce_product_options_general_product_data', 'add_custom_product_data_fields');

// Save custom meta fields when the product is saved
function save_custom_product_data_fields($product) {
    $custom_ingredients = isset($_POST['custom_ingredients']) ? sanitize_textarea_field($_POST['custom_ingredients']) : '';
    $custom_resources = isset($_POST['custom_resources']) ? sanitize_textarea_field($_POST['custom_resources']) : '';

    $product->update_meta_data('custom_ingredients', $custom_ingredients);
    $product->update_meta_data('custom_resources', $custom_resources);
}
add_action('woocommerce_admin_process_product_object', 'save_custom_product_data_fields');

// Add custom tabs to single product pages
function custom_product_tabs($tabs) {
    global $post;

    // Custom Meta Field for "Ingredients"
    $ingredients = get_post_meta($post->ID, 'custom_ingredients', true);
    if (!empty($ingredients)) {
        $tabs['ingredients_tab'] = array(
            'title'     => __('Ingredients', 'woocommerce'),
            'priority'  => 25,
            'callback'  => 'custom_product_ingredients_tab_content'
        );
    }

    // Custom Meta Field for "Educational Resources"
    $educational_resources = get_post_meta($post->ID, 'custom_resources', true);
    if (!empty($educational_resources)) {
        $tabs['educational_resources_tab'] = array(
            'title'     => __('Educational Resources', 'woocommerce'),
            'priority'  => 30,
            'callback'  => 'custom_product_educational_resources_tab_content'
        );
    }

    return $tabs;
}
add_filter('woocommerce_product_tabs', 'custom_product_tabs');

// Callback function to display custom "Ingredients" tab content
function custom_product_ingredients_tab_content() {
    global $post;

    $ingredients = get_post_meta($post->ID, 'custom_ingredients', true);

    // Output the "Ingredients" tab content
    if (!empty($ingredients)) {
        echo '<h2>' . __('Ingredients', 'woocommerce') . '</h2>';
        echo '<p>' . $ingredients . '</p>';
    }
}

// Callback function to display custom "Educational Resources" tab content
function custom_product_educational_resources_tab_content() {
    global $post;

    $educational_resources = get_post_meta($post->ID, 'custom_resources', true);

    // Output the "Educational Resources" tab content
    if (!empty($educational_resources)) {
        echo '<h2>' . __('Educational Resources', 'woocommerce') . '</h2>';
        echo '<p>' . $educational_resources . '</p>';
    }
}

Step 3:

To override WooCommerce ‘sanitizing’ the content entered in custom meta fields, we need to filter the content before it’s saved to the database and tell WooCommerce not to sanitize our custom meta fields. We can use the wp_kses_post function again to filter the content but this time without any restrictions. Let’s modify the save_custom_product_data_fields function to accomplish this.

// Save custom meta fields when the product is saved
function save_custom_product_data_fields($product) {
    $custom_ingredients = isset($_POST['custom_ingredients']) ? wp_kses_post($_POST['custom_ingredients']) : '';
    $custom_resources = isset($_POST['custom_resources']) ? wp_kses_post($_POST['custom_resources']) : '';

    $product->update_meta_data('custom_ingredients', $custom_ingredients);
    $product->update_meta_data('custom_resources', $custom_resources);
}
add_action('woocommerce_admin_process_product_object', 'save_custom_product_data_fields');
The full code that we are utilizing in functions.php is below:

In this example we have tabs for ingredients and educational resources. We’ve found that adding 2 or fewer tabs is optimal for this code to work.

// Save custom meta fields when the product is saved
function save_custom_product_data_fields($product) {
    $custom_ingredients = isset($_POST['custom_ingredients']) ? wp_kses_post($_POST['custom_ingredients']) : '';
    $custom_resources = isset($_POST['custom_resources']) ? wp_kses_post($_POST['custom_resources']) : '';

    $product->update_meta_data('custom_ingredients', $custom_ingredients);
    $product->update_meta_data('custom_resources', $custom_resources);
}
add_action('woocommerce_admin_process_product_object', 'save_custom_product_data_fields');


// Add custom tabs to single product pages
function custom_product_tabs($tabs) {
    global $post;

    // Custom Meta Field for "Ingredients"
    $ingredients = get_post_meta($post->ID, 'custom_ingredients', true);
    if (!empty($ingredients)) {
        $tabs['ingredients_tab'] = array(
            'title'     => __('Ingredients', 'woocommerce'),
            'priority'  => 25,
            'callback'  => 'custom_product_ingredients_tab_content'
        );
    }

    // Custom Meta Field for "Resources"
    $educational_resources = get_post_meta($post->ID, 'custom_resources', true);
    if (!empty($educational_resources)) {
        $tabs['educational_resources_tab'] = array(
            'title'     => __('Resources', 'woocommerce'),
            'priority'  => 30,
            'callback'  => 'custom_product_educational_resources_tab_content'
        );
    }

	
	// Custom Meta Field for "Warranty"
    $warranty = get_post_meta($post->ID, 'custom_warranty', true);
    if (!empty($warranty)) {
        $tabs['warranty_tab'] = array(
            'title'     => __('Warranty', 'woocommerce'),
            'priority'  => 35,
            'callback'  => 'custom_product_warranty_tab_content'
        );
    }

	// Custom Meta Field for "Additional Information"
    $additional_info = get_post_meta($post->ID, 'custom_additional_info', true);
    if (!empty($additional_info)) {
        $tabs['additional_info_tab'] = array(
            'title'     => __('Additional Information', 'woocommerce'),
            'priority'  => 40,
            'callback'  => 'custom_product_additional_info_tab_content'
        );
    }
	
    return $tabs;
}
add_filter('woocommerce_product_tabs', 'custom_product_tabs');

// Callback function to display custom "Ingredients" tab content
function custom_product_ingredients_tab_content() {
    global $post;

    $ingredients = get_post_meta($post->ID, 'custom_ingredients', true);

    // Output the "Ingredients" tab content
    if (!empty($ingredients)) {
        echo '<h2>' . __('Ingredients', 'woocommerce') . '</h2>';
        echo wp_kses_post($ingredients); // Use wp_kses_post to allow HTML
    }
}

// Callback function to display custom "Educational Resources" tab content
function custom_product_educational_resources_tab_content() {
    global $post;

    $educational_resources = get_post_meta($post->ID, 'custom_resources', true);

    // Output the "Educational Resources" tab content
    if (!empty($educational_resources)) {
        echo '<h2>' . __('Educational Resources', 'woocommerce') . '</h2>';
        echo wp_kses_post($educational_resources); // Use wp_kses_post to allow HTML
    }
}

// Callback function to display custom "Warranty" tab content
function custom_product_warranty_tab_content() {
    global $post;

    $warranty = get_post_meta($post->ID, 'custom_warranty', true);

    // Output the "Warranty" tab content
    if (!empty($warranty)) {
        echo '<h2>' . __('Warranty', 'woocommerce') . '</h2>';
        echo wp_kses_post($warranty); // Use wp_kses_post to allow HTML
    }
}

// Callback function to display custom "Additional Infomation" tab content
function custom_product_additional_info_tab_content() {
    global $post;

    $additional_info = get_post_meta($post->ID, 'custom_additional_info', true);

    // Output the "Additional Information" tab content
    if (!empty($additional_info)) {
        echo '<h2>' . __('Additional Information', 'woocommerce') . '</h2>';
        echo wp_kses_post($additional_info); // Use wp_kses_post to allow HTML
    }
}

Additional:

If you have physical products and have entered in their weight and dimensions, but don’t want this information appearing on a product page, the following code will allow you to remove the default Additional Information tab from WooCommerce products when the weight and dimensions are added.

//  Remove Addtional Info tab From Single product Page 
     add_filter( 'woocommerce_product_tabs', 'bbloomer_remove_product_tabs', 9999 );
          function bbloomer_remove_product_tabs( $tabs ) {
             unset( $tabs['additional_information'] ); 
              return $tabs;
         }

Watch the full Tutorial: