WooCommerce

Custom Checkout Fields in the Block Era, End to End

Adding a delivery date or gift message to the WooCommerce checkout block, and getting the value to the order screen, the emails.

6 min read
WooCommerceCheckoutBlock Editor
1,028 words6 min read

The whole path, not just the field

Adding a field to the checkout is a quarter of the job. The value has to be validated, saved to the order, shown to staff in the admin, included in the customer's and the shop's emails, printed on the packing slip, and passed to whatever fulfilment or accounting system reads orders. A field that appears on the checkout and vanishes afterwards is the most common half-finished customisation on WooCommerce stores.

Here is the full path for the checkout block, using the Additional Checkout Fields API, with a worked example: a preferred delivery date and a gift message.

Register the fields

PHP
add_action( 'woocommerce_init', function () {
  if ( ! function_exists( 'woocommerce_register_additional_checkout_field' ) ) return;

  woocommerce_register_additional_checkout_field( [
    'id'         => 'acme/delivery-date',
    'label'      => 'Preferred delivery date',
    'location'   => 'order',
    'type'       => 'text',
    'required'   => false,
    'attributes' => [
      'placeholder'  => 'YYYY-MM-DD',
      'autocomplete' => 'off',
      'pattern'      => '\d{4}-\d{2}-\d{2}',
    ],
  ] );

  woocommerce_register_additional_checkout_field( [
    'id'         => 'acme/gift-message',
    'label'      => 'Gift message (optional)',
    'location'   => 'order',
    'type'       => 'text',
    'attributes' => [ 'maxLength' => 160 ],
  ] );
} );

location => 'order' puts them in the "Additional information" step. contact places a field beside email; address adds it to billing and shipping addresses and saves it against the customer for reuse. Field IDs are namespaced (acme/...) so they cannot collide with core or other plugins.

The block has no native date picker type; a text field with a pattern and placeholder is the supported route. A real date picker is a JavaScript fill, covered in the checkout block customisation article.

Validate

PHP
add_action( 'woocommerce_validate_additional_field', function ( WP_Error $errors, $key, $value ) {
  if ( 'acme/delivery-date' !== $key || '' === $value ) return;

  $date = DateTime::createFromFormat( 'Y-m-d', $value );
  if ( ! $date || $date->format( 'Y-m-d' ) !== $value ) {
    $errors->add( 'acme_bad_date', 'Please enter the delivery date as YYYY-MM-DD.' );
    return;
  }
  if ( $date < new DateTime( '+2 days' ) ) {
    $errors->add( 'acme_too_soon', 'Delivery dates need at least two days\' notice.' );
  }
  if ( in_array( (int) $date->format( 'N' ), [ 6, 7 ], true ) ) {
    $errors->add( 'acme_weekend', 'We do not deliver at weekends.' );
  }
}, 10, 3 );

add_filter( 'woocommerce_sanitize_additional_field', function ( $value, $key ) {
  return 'acme/gift-message' === $key ? sanitize_textarea_field( $value ) : $value;
}, 10, 2 );

Errors surface inline in the block. Validation runs server-side on submit, so it holds even if the browser's pattern is bypassed.

Saving

Nothing to do. The API stores order and contact fields against the order and address fields against both the order address and the customer profile. Read them with:

PHP
$date = $order->get_additional_field_value( 'acme/delivery-date' );

Under the hood they are order meta with a _wc_other/ or _wc_billing/ style prefix; use the accessor, not the meta key.

Showing in the admin

Also automatic: additional fields render in the order edit screen's "Additional fields" panel. If you want them more prominent, add an order meta box:

PHP
add_action( 'add_meta_boxes', function () {
  $screen = wc_get_container()->get( \Automattic\WooCommerce\Internal\DataStores\Orders\CustomOrdersTableController::class )->custom_orders_table_usage_is_enabled()
    ? wc_get_page_screen_id( 'shop-order' ) : 'shop_order';
  add_meta_box( 'acme-delivery', 'Delivery', function ( $post_or_order ) {
    $order = $post_or_order instanceof WC_Order ? $post_or_order : wc_get_order( $post_or_order->ID );
    echo '<p><strong>Date:</strong> ' . esc_html( $order->get_additional_field_value( 'acme/delivery-date' ) ?: 'Not specified' ) . '</p>';
    echo '<p><strong>Gift message:</strong> ' . esc_html( $order->get_additional_field_value( 'acme/gift-message' ) ?: 'None' ) . '</p>';
  }, $screen, 'side', 'high' );
} );

The screen ID dance handles stores with and without High-Performance Order Storage. Also consider a column on the orders list for the delivery date, via manage_woocommerce_page_wc-orders_columns (HPOS) or manage_edit-shop_order_columns.

Emails

Customers should see what they entered; the shop needs it to fulfil. Add to every relevant email after the order details:

PHP
add_action( 'woocommerce_email_after_order_table', function ( $order, $sent_to_admin, $plain_text ) {
  $date = $order->get_additional_field_value( 'acme/delivery-date' );
  $msg  = $order->get_additional_field_value( 'acme/gift-message' );
  if ( ! $date && ! $msg ) return;

  if ( $plain_text ) {
    if ( $date ) echo "Preferred delivery date: $date\n";
    if ( $msg )  echo "Gift message: $msg\n";
    return;
  }
  echo '<h2>Delivery</h2><table cellspacing="0" cellpadding="6" style="width:100%;border:1px solid #eee">';
  if ( $date ) echo '<tr><th style="text-align:left">Preferred date</th><td>' . esc_html( $date ) . '</td></tr>';
  if ( $msg )  echo '<tr><th style="text-align:left">Gift message</th><td>' . esc_html( $msg ) . '</td></tr>';
  echo '</table>';
}, 10, 3 );

Test with a real order to each email type: new order (admin), processing (customer), completed.

Packing slips and invoices

PDF invoice plugins expose their own hooks. For WooCommerce PDF Invoices & Packing Slips: wpo_wcpdf_after_order_details, with $document_type to target packing slips only. Print the gift message on the slip so the picker includes it; print the delivery date so dispatch sees it.

Integrations

Anything reading orders via the REST API sees additional fields in the order's meta_data. Fulfilment and accounting connectors usually map custom meta by key; provide them the stored key (visible in the order's meta on the admin screen, or via wc_get_order( $id )->get_meta_data()). For webhooks you write yourself, read the accessor and include the values explicitly in the payload.

The "thank you" page and My Account

Customers should see their choices after ordering:

PHP
add_action( 'woocommerce_order_details_after_order_table', function ( $order ) {
  $date = $order->get_additional_field_value( 'acme/delivery-date' );
  if ( $date ) echo '<p><strong>Preferred delivery date:</strong> ' . esc_html( $date ) . '</p>';
} );

The checklist

  • Registered with a namespaced ID, in the right location.
  • Server-side validation and sanitisation.
  • Visible on the admin order screen, and in a list column if staff filter by it.
  • Included in admin and customer emails, HTML and plain text.
  • On the packing slip if fulfilment needs it.
  • Mapped for any integration that reads orders.
  • Shown on the thank-you page and in My Account.
  • Tested with a real order on staging, end to end.

Where this sits

Following the value through to the packing slip is the difference between a field and a feature, and it is how custom fields are scoped in our WooCommerce work: not "add a field to the checkout" but "capture X at checkout and get it to everyone who needs it".

All writingHire me for this