Custom Fields in WooCommerce

One way or another working with WooCommerce you wind up needing custom fields and preferably Advanced Custom Fields or ACF fields. This tutorial will talk about (advanced) custom fields in WooCommerce and the many way these can be used. This might wind up being a long post and lots or resources will be used and referred to so bear with me.

Table of Contents

Basic Single Product Page Display

What if you want to add an ACF field with your own code to the theme? Well that is just like adding any other advanced custom field type to any template really. As shown at the beginning you just need to focus on the correct product type and field type and then load the data you entered in the backend properly there.

To add an ACF field to the single product page in WooCommerce after the summary you can use a basic setup like this:

// Add ACF Info to Product display page 
add_action( 'woocommerce_after_single_product_summary', "ACF_product_content", 10 ); 
function ACF_product_content(){      
echo '<h2> ACF Content </h2>';      
if (function_exists('the_field')){     
echo '<p>Woohoo, the_field function exists! </p>';     
the_field('test_field');   
}    
}

It uses the WooCommerce woocommerce_after_single_product_summary hook to add information after the product description. And there are more hooks in WooCommerce for other locations of course.

NB The actual code to load the field in the backend is not mentioned here. This as that is quite easily done using the ACF GUI.

See https://support.advancedcustomfields.com/forums/topic/acf-with-woocommerce/

ACF & Custom Tabs

You can also use advanced custom fields to add custom tabs to WooCommerce. This is actually often needed. Many products tend to require extra information in a custom tab. Liquid Web wrote adding custom tabs with ACF. What they do is create a repeater field using a custom file added to the theme. Here is a forked snippet for the repeater field:

<?php
 if( function_exists('acf_add_local_field_group') ):
 acf_add_local_field_group(array (
     'key' => 'acf_product_options',
     'title' => 'Product Options',
     'fields' => array (
         array (
             'key' => 'acf_product_options_tabbedcontent_label',
             'label' => 'Tabbed Content',
             'name' => '',
             'type' => 'tab',
             'instructions' => '',
             'required' => 0,
             'conditional_logic' => 0,
             'wrapper' => array (
                 'width' => '',
                 'class' => '',
                 'id' => '',
             ),
             'placement' => 'top',
             'endpoint' => 0,
         ),
         array (
             'key' => 'acf_product_options_tabbedcontent_tabs',
             'label' => 'Tabs',
             'name' => 'tabs',
             'type' => 'repeater',
             'instructions' => '',
             'required' => 0,
             'conditional_logic' => 0,
             'wrapper' => array (
                 'width' => '',
                 'class' => '',
                 'id' => '',
             ),
             'min' => '',
             'max' => '',
             'layout' => 'row',
             'button_label' => 'Add Tab',
             'sub_fields' => array (
                 array (
                     'key' => 'acf_product_options_tabbedcontent_tab_title',
                     'label' => 'Tab Title',
                     'name' => 'tab_title',
                     'type' => 'text',
                     'instructions' => '',
                     'required' => 0,
                     'conditional_logic' => 0,
                     'wrapper' => array (
                         'width' => '',
                         'class' => '',
                         'id' => '',
                     ),
                     'default_value' => '',
                     'placeholder' => '',
                     'prepend' => '',
                     'append' => '',
                     'maxlength' => '',
                     'readonly' => 0,
                     'disabled' => 0,
                 ),
                 array (
                     'key' => 'acf_product_options_tabbedcontent_tab_content',
                     'label' => 'Tab Content',
                     'name' => 'tab_content',
                     'type' => 'wysiwyg',
                     'instructions' => '',
                     'required' => 0,
                     'conditional_logic' => 0,
                     'wrapper' => array (
                         'width' => '',
                         'class' => '',
                         'id' => '',
                     ),
                     'default_value' => '',
                     'tabs' => 'all',
                     'toolbar' => 'full',
                     'media_upload' => 1,
                 ),
             ),
         ),
     ),
     'location' => array (
         array (
             array (
                 'param' => 'post_type',
                 'operator' => '==',
                 'value' => 'product',
             ),
         ),
     ),
     'menu_order' => 0,
     'position' => 'normal',
     'style' => 'default',
     'label_placement' => 'top',
     'instruction_placement' => 'label',
     'hide_on_screen' => '',
 ));
 endif;

Then they load the custom tab with another function in functions.php. This code adds the extra WooCommerce product tab and then loads the repeater field inside the tab. Here is a forked snippet for this WooCommerce product tab addition.

<?php
function hwid_load_custom_tab( $tab_key, $tab_info ) {
	echo apply_filters( 'the_content', $tab_info['tabContent'] );
}

function hwid_add_content_tabs( $tabs ) {

	global $post;

	$custom_tabs = get_field( 'tabs', $post->ID );

	foreach( $custom_tabs as $index => $tab ) {
		$tabs['customTab-' . $index] = array(
			'title' => $tab['tab_title'],
			'priority' => 20 + $index,
			'tabContent' => $tab['tab_content'],
			'callback' => 'hwid_load_custom_tab'
		);
	}

	return $tabs;
}

add_filter( 'woocommerce_product_tabs', 'hwid_add_content_tabs' );

NB Snippets made by AJ Morrris https://gist.github.com/ajmorris for Liquid Web

Booster also allows you to add custom tabs. See https://booster.io/features/woocommerce-custom-product-tabs/ . You can choose to add tabs for all product pages or for specific ones.

ACF WooCommerce Order Form

If you would like to display additional information about your products in checkout / at the order form you can read about at WP Major. Major focusses on getting ACF fields in the order form and they use the WooCommerce Product Table plugin. But it does explain how to create them for WooCommerce products so that helps

Remember, for this tutorial we’re mainly focused on taking that custom field data and getting it into a WooCommerce order form ..

Divi & Advanced Custom Fields

The Elegant Themes Divi theme as an ACF module these days. It does however not work with all the custom field types yet. It just works with single fields, tables and repeater fields.

However, since the end of 2018 Divi Builder has dynamic content:

Not only does Divi support that use of standard dynamic WordPress content, it also supports the use of custom field data. Whether you have created your own custom fields, or registered a new custom field with a plugin like Advanced Custom Fields, that dynamic data can now be used within the Divi Builder and connected to any module content area.

Advanced Custom Fields to WooCommerce Attributes

Sometimes you need to add a custom field to a product attribute.

A third and important way to group products is to use attributes. There are two uses of this data type that are relevant for WooCommerce: WooCommerce widgets and variable products

Jordan Smith came up with some nice code for that. Snippet forked and added below. This code you can add to your child theme or basic theme’s functions.php. It ads an ACF custom rule type, rule values and then ads it to product attributes.

<?php 
 // Adds a custom rule type.
 add_filter( 'acf/location/rule_types', function( $choices ){
     $choices[ __("Other",'acf') ]['wc_prod_attr'] = 'WC Product Attribute';
     return $choices;
 } );
 // Adds custom rule values.
 add_filter( 'acf/location/rule_values/wc_prod_attr', function( $choices ){
     foreach ( wc_get_attribute_taxonomies() as $attr ) {
         $pa_name = wc_attribute_taxonomy_name( $attr->attribute_name );
         $choices[ $pa_name ] = $attr->attribute_label;
     }
     return $choices;
 } );
 // Matching the custom rule.
 add_filter( 'acf/location/rule_match/wc_prod_attr', function( $match, $rule, $options ){
     if ( isset( $options['taxonomy'] ) ) {
         if ( '==' === $rule['operator'] ) {
             $match = $rule['value'] === $options['taxonomy'];
         } elseif ( '!=' === $rule['operator'] ) {
             $match = $rule['value'] !== $options['taxonomy'];
         }
     }
     return $match;
 }, 10, 3 );

ACF Field WooCommerce Category

Adding an advanced custom field to a WooCommerce category is similar in ways to adding one to an attribute:

// step 1 add a location rule type
    add_filter('acf/location/rule_types', 'acf_wc_product_type_rule_type');
    function acf_wc_product_type_rule_type($choices) {
      // first add the "Product" Category if it does not exist
      // this will be a place to put all custom rules assocaited with woocommerce
      // the reason for checking to see if it exists or not first
      // is just in case another custom rule is added
      if (!isset($choices['Product'])) {
        $choices['Product'] = array();
      }
      // now add the 'Category' rule to it
      if (!isset($choices['Product']['product_cat'])) {
        // product_cat is the taxonomy name for woocommerce products
        $choices['Product']['product_cat_term'] = 'Product Category Term';
      }
      return $choices;
    }
    
    // step 2 skip custom rule operators, not needed
    
    
    // step 3 add custom rule values
    add_filter('acf/location/rule_values/product_cat_term', 'acf_wc_product_type_rule_values');
    function acf_wc_product_type_rule_values($choices) {
      // basically we need to get an list of all product categories
      // and put the into an array for choices
      $args = array(
        'taxonomy' => 'product_cat',
        'hide_empty' => false
      );
      $terms = get_terms($args);
      foreach ($terms as $term) {
        $choices[$term->term_id] = $term->name;
      }
      return $choices;
    }
    
    // step 4, rule match
    add_filter('acf/location/rule_match/product_cat_term', 'acf_wc_product_type_rule_match', 10, 3);
    function acf_wc_product_type_rule_match($match, $rule, $options) {
      if (!isset($_GET['tag_ID'])) {
        // tag id is not set
        return $match;
      }
      if ($rule['operator'] == '==') {
        $match = ($rule['value'] == $_GET['tag_ID']);
      } else {
        $match = !($rule['value'] == $_GET['tag_ID']);
      }
      return $match;
    }

NB Code course John Huebner ACF

ACF Below Product Image

To display an advanced custom field below the product image can be done with relative ease. We found a snippet at Business Bloomer by Rodolfo Melogi as a nice example:

/**
* @snippet       Display Advanced Custom Fields @ Single Product - WooCommerce
* @how-to        Get CustomizeWoo.com FREE
* @sourcecode    https://businessbloomer.com/?p=22015
* @author        Rodolfo Melogli
* @compatible    WooCommerce 3.5.7
* @donate $9     https://businessbloomer.com/bloomer-armada/
*/
 
add_action( 'woocommerce_product_thumbnails', 'bbloomer_display_acf_field_under_images', 30 );
 
function bbloomer_display_acf_field_under_images() {
echo '<b>Trade Price:</b> ' . get_field('trade');
// Note: 'trade' is the slug of the ACF
}

It uses the woocommerce_product_thumbnails hook to add elements below the product thumbnails.

Product Page & Product Table Plugin

To display an ACF field on a product page you can also use ACF to create the fields and the WooCommerce Product Table Plugin. You can read all about it at Barn2 . You can use this plugin to show a lot of product data on top of standard ones.

  • Product image, name, price
  • Short or long description
  • Categories and tags
  • Attributes and variations
  • Star rating from reviews
  • Embedded audio and video

Product Page Admin Only Note

Sometimes the end user wants to add notes in the backend for his use only. A field that is only shown in the admin area. In the admin area for a specific product. How would you go about this? Well, you do of course not have to print / display fields in the frontend so if you do not load them there you can just create them using the advanced field interface. Another way, if somehow your theme autoloads all ACF fields, is to hide them frontend with CSS.

I would use a text area or a field as ACF field type if the note is very short. That would suffice to add a note in the backend.

WC Register Form ACF Fields

To add fields to the WooCommerce Account registration form you could use https://wordpress.org/plugins/acf-woocommerce-account-fields/ . For us it did not work well though. Seems to be outdated somewhat.

You can also add custom ones using your own code. See https://stackoverflow.com/a/49054519/460885 where the awesome LoicTheAztec use the WooCommerce hook woocommerce_register_form to add fields and where he does validation as well.

And to save the form custom fields as well we use the following below as shown in SO thread:

// To save WooCommerce registration form custom fields.
 add_action( 'woocommerce_created_customer', 'wc_save_registration_form_fields' );
 function wc_save_registration_form_fields( $customer_id ) {
     if ( isset($_POST['role']) ) {
         if( $_POST['role'] == 'reseller' ){
             $user = new WP_User($customer_id);
             $user->set_role('reseller');
         }
     }
 }

NB This is similar to https://wpvilla.in/assign-specific-role-on-registration/

WooCommerce Checkout Fields

Here is another example showing a field at the end of the WooCommerce registration form’s notes or on checkout:

/**
 Add custom fields to user / checkout
 */
 add_action( 'woocommerce_after_order_notes', 'my_custom_checkout_field' ); 
 function my_custom_checkout_field( $checkout ) {
 echo '<div id="bv_custom_checkout_field"><h2>Measurements</h2>'; /* Weight */ woocommerce_form_field( 'weight_customer', array(     'type'          => 'text',     'class'         => array('my-class form-row-wide'),     'label'         => __('Your weight'),     'placeholder'   => __('Your weight'), ), get_user_meta(  get_current_user_id(),'weight_customer' , true  ) ); echo '</div>';
 }
 /**
 Verification 
 */
 add_action('woocommerce_checkout_process', 'my_custom_checkout_field_process'); 
 function my_custom_checkout_field_process() {
     // Check 
     if ( ! $_POST['weight_customer'] )
         wc_add_notice( __( 'Do not forget weight.' ), 'error' );
 }
 Update field
 */
 add_action( 'woocommerce_checkout_update_order_meta', 'my_custom_checkout_field_update_order_meta' ); 
 function my_custom_checkout_field_update_order_meta( $order_id ) {
     if ( ! empty( $_POST['weight_customer'] ) ) {
         update_user_meta( get_current_user_id(), 'weight_customer', sanitize_text_field( $_POST['weight_customer'], '' ));
     }
 }

Here woocommerce_after_order_notes and woocommerce_checkout_update_order_meta are the hooks used. So data is added after the order notes and the woocommerce checkout update order meta hook is used to store the extra fields. For other location see a good visual guide at https://businessbloomer.com/woocommerce-visual-hook-guide-checkout-page/ .

Snippet source Max @ ACF Forum

Custom Checkout Fields

If you need to make tweak to fields at /checkout you can use stuff discussed at https://docs.woocommerce.com/document/tutorial-customising-checkout-fields-using-actions-and-filters/ . To for example change the order comment placeholder you use:

// Hook in
add_filter( 'woocommerce_checkout_fields' , 'custom_override_checkout_fields' );

// Our hooked in function - $fields is passed via the filter!
function custom_override_checkout_fields( $fields ) {
     $fields['order']['order_comments']['placeholder'] = 'My new placeholder';
     return $fields;
}

Now if you want to add more billing fields and or add them before or after exisitng you can use something like

 /**
 * Simple checkout field addition example.
 * 
 * @param  array $fields List of existing billing fields.
 * @return array         List of modified billing fields.
 */
function jeroensormani_add_checkout_fields( $fields ) {
  $fields['billing_FIELD_ID'] = array(
      'label'        => __( 'FIELD LABEL' ),
      'type'        => 'text',
      'class'        => array( 'form-row-wide' ),
      'priority'     => 35,
      'required'     => true,
  );

  return $fields;
}
add_filter( 'woocommerce_billing_fields', 'jeroensormani_add_checkout_fields' );

If you want to position them you need to know the priorities of the current ones:

  • First name – 10
  • Last name – 20
  • Company name – 30
  • Country – 40
  • Street address – 50
  • Apartment, suite, unit etc. (optional) – 60
  • Town / City – 70
  • State – 80
  • Postcode / ZIP – 90
  • Phone – 100
  • Email – 110

Also, do not forget validation. Here a basic one for checking if a real number is entered. Good for a VAT number for example

**
 * Add custom field validation for BTW or KvK Number
 */
function js_custom_checkout_field_validation( $data, $errors ) {
  foreach ( WC()->checkout()->get_checkout_fields() as $fieldset_key => $fieldset ) {
      foreach ( $fieldset as $key => $field ) {

          if ( isset( $field['validate'] ) && in_array( 'btw-number', $field['validate'] ) ) {
              if ( ! empty( $data[ $key ] ) && ! preg_match( '/[a-z0-9]{10}/', $data[ $key ] ) ) {
                  $errors->add( 'validation', 'Looks like your club number is invalid.' );
              }
          }
      }
  }

}
add_action( 'woocommerce_after_checkout_validation', 'js_custom_checkout_field_validation', 10, 2 );

NB source Jeroen Sormani

NBB Checkout Manager Plugin is very useful too although paid if in need of conditionals

NBBB There is Also WooCommerce Booster https://booster.io/features/woocommerce-checkout-customization/ , but no conditionals offered.

My Account / WooCommerce Registration

Like any WooCommerce page there are multiple hooks to adjust or add things to /my-account. See https://businessbloomer.com/woocommerce-visual-hook-guide-account-pages/.

To adjust or add more fields things are more complicated like for the checkout, but this is also possible. Business Bloomer has a snippet to add first and last name for example:

/**
 @snippet       Add First & Last Name to My Account Register Form - WooCommerce
 @how-to        Get CustomizeWoo.com FREE
 @sourcecode    https://businessbloomer.com/?p=21974
 @author        Rodolfo Melogli
 @credits       Claudio SM Web
 @compatible    WC 3.5.2
 @donate $9     https://businessbloomer.com/bloomer-armada/
 */ 
 ///////////////////////////////
 // 1. ADD FIELDS
 add_action( 'woocommerce_register_form_start', 'bbloomer_add_name_woo_account_registration' );
 function bbloomer_add_name_woo_account_registration() {
     ?>
 <p class="form-row form-row-first"> <label for="reg_billing_first_name"><?php _e( 'First name', 'woocommerce' ); ?> <span class="required">*</span></label> <input type="text" class="input-text" name="billing_first_name" id="reg_billing_first_name" value="<?php if ( ! empty( $_POST['billing_first_name'] ) ) esc_attr_e( $_POST['billing_first_name'] ); ?>" /> </p> <p class="form-row form-row-last"> <label for="reg_billing_last_name"><?php _e( 'Last name', 'woocommerce' ); ?> <span class="required">*</span></label> <input type="text" class="input-text" name="billing_last_name" id="reg_billing_last_name" value="<?php if ( ! empty( $_POST['billing_last_name'] ) ) esc_attr_e( $_POST['billing_last_name'] ); ?>" /> </p> <div class="clear"></div> <?php
 }
 ///////////////////////////////
 // 2. VALIDATE FIELDS
 add_filter( 'woocommerce_registration_errors', 'bbloomer_validate_name_fields', 10, 3 );
 function bbloomer_validate_name_fields( $errors, $username, $email ) {
     if ( isset( $_POST['billing_first_name'] ) && empty( $_POST['billing_first_name'] ) ) {
         $errors->add( 'billing_first_name_error', ( 'Error: First name is required!', 'woocommerce' ) );
     }
     if ( isset( $_POST['billing_last_name'] ) && empty( $_POST['billing_last_name'] ) ) {
         $errors->add( 'billing_last_name_error', ( 'Error: Last name is required!.', 'woocommerce' ) );
     }
     return $errors;
 }
 ///////////////////////////////
 // 3. SAVE FIELDS
 add_action( 'woocommerce_created_customer', 'bbloomer_save_name_fields' );
 function bbloomer_save_name_fields( $customer_id ) {
     if ( isset( $_POST['billing_first_name'] ) ) {
         update_user_meta( $customer_id, 'billing_first_name', sanitize_text_field( $_POST['billing_first_name'] ) );
         update_user_meta( $customer_id, 'first_name', sanitize_text_field($_POST['billing_first_name']) );
     }
     if ( isset( $_POST['billing_last_name'] ) ) {
         update_user_meta( $customer_id, 'billing_last_name', sanitize_text_field( $_POST['billing_last_name'] ) );
         update_user_meta( $customer_id, 'last_name', sanitize_text_field($_POST['billing_last_name']) );
     }
 }

NB Also see https://github.com/woocommerce/woocommerce/issues/7667 and https://www.cloudways.com/blog/add-woocommerce-registration-form-fields/

To work with conditionals things get more complicated and of course radio buttons or select boxes are also a bit tougher still. You could use jQuery like this for example showing a field when radio button toggled:

$('.customer-type-radio input[type="radio"]').on('click', function () {
         $('.company-field').slideToggle();
 });

For code above by Business Bloomer you would need to focus on other html fields of course, but often after making choices like checkboxes or radio buttons you would like to toggle other fields.

WC Booster has some basic options including adding a user role select box https://booster.io/features/woocommerce-my-account/ . Does not seem to offer custom fields though.

There is also Yithemes Yith Woocommerce Customiz My Account Page . Does cost another €54.99. But that is really for the my-account overview for logged in users. For the WooCommerce Registration Form https://woocommerce.com/products/custom-user-registration-fields-for-woocommerce/ is recommended. It costs $49 USD.

Shop Page ACF

If you would like to add a new ACF location to do stuff on the WooCommerce Shop Page you can use

add_filter( 'acf/location/rule_values/page_type', function ( $choices ) {
  $choices['woo_shop_page'] = 'WooCommerce Shop Page';
  return $choices;
});
add_filter( 'acf/location/rule_match/page_type', function ( $match, $rule, $options ) {
  if ( $rule['value'] == 'woo_shop_page' && isset( $options['post_id'] ) ){
     if ( $rule['operator'] == '==' ){
       $match = ( $options['post_id'] == wc_get_page_id( 'shop' ) );
      }
     if ( $rule['operator'] == '!=' ){
       $match = ( $options['post_id'] != wc_get_page_id( 'shop' ) );
     }
  }
  return $match;
}, 10, 3 );