In this post you’ll learn how to change, remove or reposition default checkout fields in WooCommerce, and how to add your own custom field.

The checkout fields filter

WooCommerce offers a filter for all checkout fields: woocommerce_checkout_fields. There is also dedicated filters for billing and shipping fields; woocommerce_billing_fields and woocommerce_shipping_fields but they both end up in the abovementioned first filter.

The fields in woocommerce_checkout_fields are structured in a multidimensional array like so:

  • billing
    • billing_first_name
    • billing_last_name
    • billing_company
    • billing_address_1
    • billing_address_2
    • billing_city
    • billing_postcode
    • billing_country
    • billing_state
    • billing_email
    • billing_phone
  • order
    • order_comments
  • shipping
    • shipping_first_name
    • shipping_last_name
    • shipping_company
    • shipping_address_1
    • shipping_address_2
    • shipping_city
    • shipping_postcode
    • shipping_country
    • shipping_state
  • account
    • account_username
    • account_password
    • account_password-2

Each field is an array containing the following settings (not all fields contains all properties):

  • label: The field’s visible label in checkout
  • placeholder: The input’s placeholder
  • type: Defines the field type. If not set, it defaults to text input, otherwise it can be select, tel, email, textarea, etc.
  • required: A boolean whether or not the field is required or not
  • class: Classes which gets applied to the wrapper element of the field. Keep in mind that this is an array.
  • priority: Integer informing WooCommerce of the field’s order

Some fields have additional properties which are uniquely customized for certain fields, and usually not something you need to think about.

Changing default fields

By adding a filter to woocommerce_checkout_fields and looking at the overview above, it should be pretty easy to understand how to change the existing fields.

Here’s an example of how to change the label on the phone field:

add_filter('woocommerce_checkout_fields', function($fields) {
	$fields['billing']['billing_phone']['label'] = __('Mobile phone', 'textdomain');
	return $fields;
});

Or maybe you’d like to change the required property making the field optional (or vice versa)?

add_filter('woocommerce_checkout_fields', function($fields) {
	$fields['billing']['billing_phone']['required'] = false;
	return $fields;
});

However if you want to edit the address fields (applies especially to _address_1, _postcode and _city) you might need to use a different filter; namely woocommerce_default_address_fields. As argument to this filter the array is no longer structured with billing and shipping keys, and the field keys are missing the prefixed “billing_” or “shipping_” parts. Address 1 is for example in this filter under the key address_1.

Here’s an example changing postcode’s label:

add_filter('woocommerce_default_address_fields', function($address_fields) {
	$address_fields['postcode']['label'] = __('Postcode', 'textdomain');
	return $address_fields;
});

Removing default fields

Removing default fields is easily done by using PHP’s unset() array function in the woocommerce_checkout_fields filter. For example:

add_filter('woocommerce_checkout_fields', function($fields) {
	unset($fields['billing']['billing_city']);
	return $fields;
});

Reordering fields

Reordering fields is done by changing the property priority on the fields. This property was recently added (WooCommerce 3+) and made reordering much easier than before where you had to rearrange the array.

The lower the number, the higher up it comes. Each field’s priority gives some leeway for moving fields inbetween, for example first name starts at 10, last name at 20, and company at 30. If you wanted a field after last name and before company, you could put the priority at 25.

Example of moving phone field before address:

add_filter('woocommerce_checkout_fields', function($fields) {
	$fields['billing']['billing_phone']['priority'] = 35;
	return $fields;
});

The same special case for address fields as mentioned earlier applies here as well. If you wanted to change for example city’s priority, use the filter woocommerce_default_address_fields.

add_filter('woocommerce_default_address_fields', function($address_fields) {	
	$address_fields['city']['priority'] = 45;
	return $address_fields;
});

Note: WooCommerce will often override the priority on the postcode field (setting it to 65) depending on country.

Adding custom fields to checkout

You have two options in adding custom fields; you can either add a custom field to either billing or shipping array through the woocommerce_checkout_fields filter, and WooCommerce will handle all about processing and saving it onto the order. Alternatively you could use actions in checkout (for example woocommerce_after_order_notes) to add a field, but in this case you need to write code to process and save it manually.

Method 1: Adding custom field to billing or shipping using checkout filter

The easy method is using the same filter as we’ve used all along; woocommerce_checkout_fields and simply add a new array element.

WooCommerce’s documentation is not making this clear, but this method only works for adding fields to ‘billing’ or ‘shipping’. Also, your key/name of field must be prefixed with the corresponding parent key; for example for adding a field ‘my_custom_input‘ to the billing array, it needs to be named ‘billing_my_custom_input‘. Otherwise WooCommerce won’t save your field.

Here’s an example of adding a non-required custom select/dropdown for billing, positioned right before phone. By setting ‘type‘ to ‘select‘ and providing an array for the options to the ‘options‘ property you make a select input.

add_filter('woocommerce_checkout_fields', function($fields) {
	$fields['billing']['billing_my_custom_select'] = [
		'label' => __('My custom select', 'textdomain'),
		'required' => false,
		'type' => 'select',
		'options' => [
			'' => __('Choose which you prefer', 'textdomain'),
			'dogs' => __('Dogs are best', 'textdomain'),
			'cats' => __('I prefer cats', 'textdomain')
		],
		'class' => ['form-row-wide'],
		'priority' => 85
	];
	return $fields;
});

WooCommerce will process and save this field as a part of the checkout fields automatically.

There’s no automatic way to make your field visible in order admin. You can hook onto the action woocommerce_admin_order_data_after_billing_address and manually get the meta field, using order ID from order object provided as argument to the action. Keep in mind that the value will be saved as post meta with a leading “_”. In the example above the field will be saved as “_billing_my_custom_select” in the database.

add_action('woocommerce_admin_order_data_after_billing_address', function($order) {
	$my_value = get_post_meta($order->id, '_billing_my_custom_select', true);
	if (!empty($my_value)) {
		echo '<p><strong>' . __('My custom select', 'textdomain') . ':</strong> ' . $my_value . '</p>';
	}
});

Method 2: Adding custom field using actions

If you encounter issues with the above method, or wish to position your field differently, I recommend going for this method. It’s a little more code, but you have more control. I personally prefer this method.

First step is choosing an action in checkout in which you want to add your field. This is simply a matter of preference of where you want your field to appear. Take a look in /woocommerce/templates/checkout/ template files; e.g. form-checkout.php, form-billing.php or form-shipping.php. In my example I’ve chosen the action woocommerce_after_order_notes which appears in form-shipping.php, after order notes.

You use the function woocommerce_form_field and provide the same properties as you have seen in the woocommerce_checkout_fields filter (see above). Optionally you can add HTML around your custom field, like I have done. Here is an example of adding a custom field for inputting member ID:

add_action('woocommerce_after_order_notes', function($checkout) {
	echo '<div id="my_custom_checkout_field"><h3>' . __('Members', 'textdomain') . '</h3>';

	woocommerce_form_field('member_id', [
		'type' => 'text',
		'class' => ['form-row-wide'],
		'label' => __('Member ID', 'textdomain'),
	], $checkout->get_value('member_id'));

	echo '</div>';
});

The field should now appear in checkout.

Validation of your field is optional. I’ve included an example of validation to ensure a valid member ID was provided by checking its length. For validation use the hook woocommerce_checkout_process and if you need to return an error and stop the checkout process, use wc_add_notice().

add_action('woocommerce_checkout_process', function() {
	if (!empty($_POST['member_id'])) {
		$given_member_id = $_POST['member_id'];
		if (strlen($given_member_id) < 5 || strlen($given_member_id) > 10) {
			wc_add_notice(__('Invalid member ID', 'textdomain'), 'error');
		}
	}
});

Next step is actually saving the field’s value whenever someone places an order. For this we use the action woocommerce_checkout_update_order_meta and simply saves the field as a post meta to the order ID (provided as parameter to the action), fetching the value from $_POST.

add_action('woocommerce_checkout_update_order_meta', function($order_id) {
	if (!empty($_POST['member_id'])) {
		update_post_meta($order_id, 'member_id', sanitize_text_field($_POST['member_id']));
	}
}

Your custom field should now be saved to orders.

If you want to display the field in order admin, add the same code as in method 1. I’ll include it again here, adjusted for this example’s field name:

add_action('woocommerce_admin_order_data_after_billing_address', function($order) {
	$member_id = get_post_meta($order->id, 'member_id', true);
	if (!empty($member_id)) {
		echo '<p><strong>' . __('Member ID', 'textdomain') . ':</strong> ' . $member_id . '</p>';
	}
});