The usual problem with a custom checkout field is not that it is hard to add - it is that a field added the old way stops existing once the store moves to the checkout block. The store keeps running, orders keep coming in, and the “customer number” or “preferred delivery window” field simply vanishes from the form. Nobody notices for a week, because the orders are still arriving.
That is the main trap in this topic today, and it is where you start, before writing a single line of code.
This piece is about the technical side: where a field hooks in, how to save it, where to see it, and when to pick a plugin over code. The separate question of which fields should be in the checkout at all - and why a shorter form sells better - I covered in the piece on a shorter checkout. Here I assume that decision is behind you and you know what the field is for.
First, work out which checkout you have
WooCommerce currently ships two parallel implementations of the order page, and completely different mechanisms hook into them:
- Classic checkout - a page built on the
[woocommerce_checkout]shortcode. PHP renders the form, and everything is modified with filters and actions that have been around for years. - Checkout block - a page built from blocks in the editor. JavaScript renders the form and talks to the server over the Store API. The PHP filters from the classic checkout do not work here at all.
You can check in half a minute: open the “Checkout” page in the WordPress editor. If you see a single shortcode block with [woocommerce_checkout], you are on classic. If you see a set of blocks with a form preview, you are on the block checkout.
That distinction decides everything else. New WooCommerce installs get the block checkout by default now, but a huge proportion of live stores sit on classic - either because that is how it was built, or because a plugin forced the switch back.
Classic checkout: three filters that do all the work
In the classic checkout the entire form structure lives in one array, passed through the woocommerce_checkout_fields filter. The array has three sections: billing, shipping and order (which is where order notes live).
What you do with that filter:
- Remove a field - just
unsetit from the array. - Change the label, placeholder, order or required state - by swapping the
label,placeholder,priorityandrequiredkeys. - Add your own field - by appending a new element to a section.
Two related filters worth knowing:
woocommerce_default_address_fields- modifies address fields in billing and shipping at once. If you are changing the same field in two places, this filter halves the work.woocommerce_form_field_args- goes one level lower, to the individual field at render time. Useful when you want to add an HTML attribute, for example a correctautocompletevalue.
Adding the field, though, is only the first of four steps. A field that displays but does not save is worse than no field at all - the customer fills it in and nothing comes of it.
Four steps without which the field is useless
In the classic checkout, a complete custom field implementation always means the same four hooks:
- Display - the
woocommerce_checkout_fieldsfilter, or thewoocommerce_before_order_notes/woocommerce_after_order_notesaction if you want the field outside the address sections. Render with thewoocommerce_form_field()function, not your own HTML - that way the field looks like the rest of the form and inherits the theme’s classes. - Validation - the
woocommerce_checkout_processaction. Here you check whether the value makes sense and, on error, callwc_add_notice( '...', 'error' ). Without this, a “required” field is only required visually. - Saving to the order - the
woocommerce_checkout_create_orderaction, where you store the value with$order->update_meta_data(). - Showing it where somebody will look - in the order admin (
woocommerce_admin_order_data_after_billing_address) and in the emails to you and to the customer (thewoocommerce_email_order_meta_fieldsfilter).
Step four is the one that most often gets dropped. The field saves to the database, but nobody sees it, so a month later somebody concludes “it does not work” - when it does, and the data is sitting in order meta nobody ever opens.
One technical note that matters since the new order storage (HPOS) arrived: use the order object’s methods to read and write ($order->update_meta_data(), $order->get_meta()), not the old update_post_meta() and get_post_meta() functions. The old functions work as long as the store keeps orders in the posts table - and quietly stop when somebody switches the store to the new tables.
// classic checkout: save a custom field to the order
add_action( 'woocommerce_checkout_create_order', function ( $order ) {
if ( ! empty( $_POST['delivery_window'] ) ) {
$order->update_meta_data(
'_delivery_window',
sanitize_text_field( wp_unslash( $_POST['delivery_window'] ) )
);
}
}, 10, 1 );
The checkout block: a separate API, different rules
In the block checkout you register fields with a single function: woocommerce_register_additional_checkout_field(). You give it an identifier with your own namespace (the my-plugin/field-name format), a label, a type and a location.
Three things to know before you start:
- The field types are limited. You get text, a select list and a checkbox. That narrowing is deliberate - the block checkout enforces a consistent look and accessibility, so you cannot drop arbitrary HTML into it.
- There are three locations, and they decide where the field appears and how it behaves: contact information, address (the field then appears in billing and shipping, and the value is stored with the customer’s address) and the order section.
- Saving happens on its own. You write nothing to persist the data - WooCommerce saves it to the order and to the customer profile, and shows it in the admin and in emails with no extra code. That is a big difference from the classic checkout, where those two steps are yours to write.
Validation hooks in through separate filters: one checks an individual field, the other lets you validate a whole group of fields in a given location at once (for example, one field’s dependency on another).
The practical conclusion: there is less code, but it is entirely different code. A snippet from an old blog post, pasted into a store running the block checkout, will not throw an error - it will simply do nothing. That is exactly the scenario where a field “disappears after an update”.
Conditional fields: company or private customer
The most common real requirement after adding a field is “it should only show when…”. Usually that means a tax ID visible after ticking “I want a company invoice”.
Two things have to be kept apart here:
- Hiding and showing a field is browser-side work. In the classic checkout you do it with JavaScript reacting to a change on the controlling field. In the block checkout, conditional fields are handled through visibility logic at registration.
- Validation has to be server-side, always. Hiding a field in the browser enforces nothing - anyone who wants to can submit the form without it. Put the rule “if company is ticked, the tax ID is required” into PHP validation, regardless of what the JavaScript does.
Tax IDs and invoicing are a separate topic, tightly bound to e-invoicing - I covered it in the piece on WooCommerce, ERP and e-invoicing integration.
Plugin or code
The honest answer: it depends on what you do with the data afterwards.
A checkout field editor plugin makes sense when:
- you need a few simple text fields or select lists,
- nobody in the company is going to maintain code,
- the data only has to reach the admin panel and the email, with no further processing.
It costs tens to a few hundred zlotys a year and saves a day of work. Check one thing before buying: does the plugin support the block checkout. Plenty of popular plugins in this category worked only in the classic checkout for a long time, and that is often still the case.
Code in a child theme or in your own small plugin wins when:
- the field has conditional logic or validation beyond “required”,
- the value has to travel further - to an ERP, a carrier, an invoicing system,
- you want certainty about what happens after an update, and the ability to read it.
One rule with no exceptions: never in parent theme files or in an off-the-shelf theme’s functions.php. The first theme update wipes it all. A child theme or a separate plugin, and that is that.
What not to do
Four mistakes that cost the most, and I see them regularly:
- Injecting raw HTML instead of
woocommerce_form_field(). The field looks different from the rest of the form, loses error handling, and in the block checkout will not work at all. - No sanitisation on save. A value from the form is user input, not yours. Always through
sanitize_text_field()or the equivalent for the type. - Adding fields because “they might come in handy”. Every field is seconds and doubts on the customer’s side. A field nobody in the company reads should go - why that hurts in numbers I show in technical reasons carts get abandoned.
- Testing in production. Checkout changes land at exactly the moment a customer is typing in card details. Copy the store, test there, then deploy - and always take a backup before the change.
How to test it before release
Four tests, in order, and I only close the ticket after the fourth:
- Place an order with the field filled in and check that the value appears in the order admin and in the emails to you and to the customer.
- Place an order with the field empty, to check whether validation genuinely blocks submission and whether the error message is in the right language and next to the right field.
- Check it on a phone. Most orders come from mobile, and custom fields can break the form layout or trigger the wrong keyboard (numeric for a number, email for an address).
- Check it after changing the shipping method. The checkout reloads part of the form over AJAX on every shipping change - that is the most common moment a custom field loses its entered value or disappears.
Plus one thing that is easy to forget: if the field is meant to reach a carrier or an invoice, test the whole chain, not just the field. A value saved on the order that no integration reads is still half the job. What those flows look like in practice I set out in the piece on shipping integrations.
FAQ
Why did my field disappear from the checkout after an update?
Almost always because the store moved to the block checkout. Fields added with the woocommerce_checkout_fields filter work only in the classic checkout and simply do not render in the block one, with no error at all. Check the checkout page in the editor - if you see blocks instead of the shortcode, that is the cause. You then have two routes: rewrite the field against the new API, or temporarily go back to the classic checkout with the shortcode.
Can I add a field only for certain products or categories? You can, but it is always code, not a plugin. You check the condition against the cart contents and register or skip the field on that basis. Watch out for the mixed case: the customer has a product that requires the field and one that does not - decide up front what happens then.
Where does custom field data get saved? In order meta. In the classic checkout under a key you set yourself; in the block checkout under a key generated from the field’s location and namespace. The practical consequence: this data will not automatically appear in a CSV export, in an ERP integration or on an invoice. In all of those places it has to be deliberately mapped.
Do custom fields slow the checkout down? The fields themselves do not. What slows it down is what gets bolted onto them: external JavaScript validation libraries, an API call on every keystroke, plugins loading their own CSS across the whole store. Three fields added properly make no measurable difference; one heavy form-builder plugin does.
Can I remove a field WooCommerce requires, such as country? Technically yes, but usually you should not. Country and postcode drive tax and shipping calculation - once removed, WooCommerce falls back to the store’s default location, which gives wrong amounts on cross-border sales. If you only sell in one country, setting a single allowed country in the settings makes more sense than cutting the field out of the form.
Running a store where something does not work the way it should? I build and rebuild WooCommerce stores - from a single fix to reworking the whole sales flow. Tell me what you are dealing with and I will send back a scope and a price.