Skip to content
ThemesIonic — home
WooCommerce

Editing the WooCommerce Checkout Fields

The filter still works, but it only governs the classic checkout shortcode. On a store using the checkout block it runs and changes nothing, which is why so many working snippets suddenly stopped applying.

4 min read intermediate

Before you touch the filter, confirm which checkout the store actually renders. woocommerce_checkout_fields governs the classic shortcode checkout. A store using the checkout block builds its form from a separate registry, so the filter fires, returns a modified array, and nothing on the page changes. That single distinction accounts for most snippets that "stopped working" without anyone editing them.

The shape of the array

The filter receives one nested array with four top-level groups:

Group Contains Also used by
billing Billing address and contact fields Order emails, tax location
shipping Shipping address fields Shipping zone matching
account Username and password when registering at checkout Account creation
order Order notes The order screen

Each field is itself an array of properties — label, required, placeholder, class, priority, type. Editing one means reaching two levels down:

add_filter( 'woocommerce_checkout_fields', 'ti_checkout_fields' );

function ti_checkout_fields( $fields ) {
	$fields['billing']['billing_phone']['required'] = false;
	$fields['billing']['billing_company']['label']  = __( 'Company (optional)', 'your-textdomain' );
	$fields['order']['order_comments']['placeholder'] = __( 'Delivery notes', 'your-textdomain' );

	unset( $fields['billing']['billing_address_2'] );

	return $fields;
}

Returning $fields is not optional. A callback that modifies the array and forgets the return hands WooCommerce null, and the checkout renders with no fields at all — a dramatic failure that looks like a fatal error but is not one.

Priority controls order, and it is not the hook priority

Every field carries its own priority property, and that is what determines display order within a group. It has nothing to do with the priority argument of add_filter:

$fields['billing']['billing_phone']['priority'] = 25;

Billing fields are numbered in tens, so the gaps are deliberate — an odd number like 25 slots cleanly between two defaults without renumbering anything. Reordering by rewriting the whole array instead is what produces checkouts where the postcode sits above the street.

Fields that are not safe to remove

Unsetting a field removes the input, not the dependency on the value behind it:

  • billing_country decides the tax rate and matches the shipping zone. Remove it and both fall back to store defaults, which is wrong the moment a second country is served. If the store genuinely ships to one country, set a default server-side rather than deleting the field.
  • billing_postcode feeds postcode-based shipping zones. A store using them will silently stop matching, in the way described under WooCommerce shipping setup.
  • billing_email is where every transactional message goes. Without it the order completes and the customer hears nothing, which surfaces later as emails not sending.
  • billing_first_name and billing_last_name appear on invoices and in the order list. Removing them leaves orders that are hard to identify in support.

Making a field optional is nearly always the better move than removing it, because the column still exists and the value is still stored when present.

The locale layer sits on top

After your filter runs, WooCommerce applies country-specific overrides. A field you marked optional can be forced back to required when the customer selects a country whose locale demands it, and a label you set can be replaced with a localised one.

This is why a change appears to work until someone selects a different country. The locale rules live behind woocommerce_get_country_locale, and a field that must stay optional everywhere has to be adjusted there as well as in the field array. Testing with a single country hides the entire problem.

Verify before shipping the change

Run through four checks, in this order:

  1. Confirm the checkout page renders the classic shortcode and not the block. If it is the block, stop — the filter is the wrong tool and no amount of editing will help.
  2. Load the checkout with an empty cart and a full one; some fields only render when shipping is required.
  3. Switch the country twice and confirm your required and optional states survive the locale pass.
  4. Place one real test order and open the resulting order in the admin, confirming the values you kept are stored against it.

If the form itself fails to submit after the edit, the cause is usually a removed field that validation still expects rather than the markup, and the route through that is checkout not working.

Frequently asked

The store is almost certainly rendering the checkout block rather than the classic shortcode. The block builds its form from its own field registry, so the filter runs and its return value is never read. Check which of the two the checkout page contains before debugging the snippet.
Because those are different field sets. The address forms under My Account come from woocommerce_default_address_fields, while checkout-only changes come from woocommerce_checkout_fields. Filter the former if you want both to match.
No. Tax rates and shipping zones are resolved from the customer country, so removing the field leaves both calculating against a fallback. Hide it with a default value server-side if the store ships to exactly one country.

Related guides