How to customize WooCommerce checkout fields with hooks in WordPress

Contents

Overview

This article is a comprehensive, practical tutorial that explains how to customize WooCommerce checkout fields using hooks. It covers the most common tasks: adding new fields, removing or modifying existing fields, changing order and priorities, validating input, saving values to orders and user meta, showing fields in admin and emails, and conditional/JavaScript-driven behaviors. Every example is complete and copy-paste ready for a themes functions.php or a custom plugin file. Use a child theme or custom plugin for production.

Core concepts and hooks

WooCommerce checkout fields live in an array returned by the filter woocommerce_checkout_fields. You can add, edit or remove fields by hooking into that filter. Additional hooks and actions youll commonly use:

  • woocommerce_checkout_process — server-side validation before order is created.
  • woocommerce_checkout_update_order_meta — save posted checkout fields onto the order meta.
  • woocommerce_admin_order_data_after_billing_address — display custom fields in the admin order view.
  • woocommerce_email_order_meta_fields — include custom fields in order emails.
  • woocommerce_thankyou / woocommerce_view_order — display custom fields on the thank-you and order view pages.
  • wc_add_notice() — add checkout errors or notices from PHP validation hooks.

Structure of a checkout field array

Each field is an associative array. The most commonly used keys are:

Key Meaning
type Input type: text, textarea, select, checkbox, radio, password, date, etc.
label Field label shown to the user.
placeholder Placeholder text for input types that support it.
required Boolean true/false whether field is mandatory.
class Array of classes added to the field wrapper for layout.
label_class Array of classes applied to the label element.
options For select and radio: associative array of value => label.
priority Number controlling order among fields.
default Default value for the field.

Where checkout fields live

Fields are grouped by sections in the checkout fields array:

  • billing — billing_ fields.
  • shipping — shipping_ fields.
  • account — account creation fields.
  • order — order comments, custom order fields.

Basic examples

1) Remove an existing field

Remove billing company field:

add_filter( woocommerce_checkout_fields, prefix_remove_billing_company )
function prefix_remove_billing_company( fields ) {
    if ( isset( fields[billing][billing_company] ) ) {
        unset( fields[billing][billing_company] )
    }
    return fields
}

2) Modify an existing field (placeholder, label, required)

Change the billing phone placeholder and mark as required:

add_filter( woocommerce_checkout_fields, prefix_modify_billing_phone )
function prefix_modify_billing_phone( fields ) {
    fields[billing][billing_phone][placeholder] = Enter a contact phone number
    fields[billing][billing_phone][label]       = Contact Phone
    fields[billing][billing_phone][required]    = true
    return fields
}

3) Add a new text field

Add a new required text field to the billing section and save it to the order:

add_filter( woocommerce_checkout_fields, prefix_add_billing_delivery_instructions )
function prefix_add_billing_delivery_instructions( fields ) {
    fields[billing][billing_delivery_instructions] = array(
        type        => text,
        label       => Delivery instructions,
        placeholder => Leave any delivery notes here,
        required    => false,
        class       => array( form-row-wide ),
        clear       => true,
        priority    => 120,
    )
    return fields
}

// Save to order meta
add_action( woocommerce_checkout_update_order_meta, prefix_save_delivery_instructions )
function prefix_save_delivery_instructions( order_id ) {
    if ( ! empty( _POST[billing_delivery_instructions] ) ) {
        update_post_meta( order_id, _billing_delivery_instructions, sanitize_text_field( wp_unslash( _POST[billing_delivery_instructions] ) ) )
    }
}

4) Add a select (dropdown) field

Example: preferred delivery time slot:

add_filter( woocommerce_checkout_fields, prefix_add_delivery_timeslot )
function prefix_add_delivery_timeslot( fields ) {
    fields[order][delivery_timeslot] = array(
        type        => select,
        label       => Preferred delivery time,
        required    => true,
        class       => array( form-row-wide ),
        options     => array(
                         => Select a timeslot,
            morning      => Morning (8am - 12pm),
            afternoon    => Afternoon (12pm - 4pm),
            evening      => Evening (4pm - 8pm),
        ),
        priority    => 200,
    )
    return fields
}

add_action( woocommerce_checkout_update_order_meta, prefix_save_delivery_timeslot )
function prefix_save_delivery_timeslot( order_id ) {
    if ( isset( _POST[delivery_timeslot] ) ) {
        update_post_meta( order_id, _delivery_timeslot, sanitize_text_field( wp_unslash( _POST[delivery_timeslot] ) ) )
    }
}

Validation: enforcing rules server-side

Always validate server-side in addition to client-side JS. Use the action woocommerce_checkout_process to add validation errors using wc_add_notice.

add_action( woocommerce_checkout_process, prefix_validate_delivery_timeslot )
function prefix_validate_delivery_timeslot() {
    if ( isset( _POST[delivery_timeslot] )  empty( _POST[delivery_timeslot] ) ) {
        wc_add_notice( Please choose a preferred delivery time., error )
    }

    // Example regex validation for custom text field
    if ( isset( _POST[billing_delivery_instructions] ) ) {
        value = trim( wp_unslash( _POST[billing_delivery_instructions] ) )
        if ( value !==   ! preg_match( /^[A-Za-z0-9 ,.-]{2,200}/, value ) ) {
            wc_add_notice( Delivery instructions contain invalid characters., error )
        }
    }
}

Display custom fields in the admin order view

To display saved meta in the backend order page:

add_action( woocommerce_admin_order_data_after_billing_address, prefix_display_delivery_meta_in_admin, 10, 1 )
function prefix_display_delivery_meta_in_admin( order ) {
    timeslot = get_post_meta( order->get_id(), _delivery_timeslot, true )
    instructions = get_post_meta( order->get_id(), _billing_delivery_instructions, true )

    if ( timeslot ) {
        echo ltpgtltstronggtPreferred delivery:lt/stronggt  . esc_html( timeslot ) . lt/pgt
    }
    if ( instructions ) {
        echo ltpgtltstronggtDelivery notes:lt/stronggt  . esc_html( instructions ) . lt/pgt
    }
}

Include custom fields in order emails

Simple approach: use the filter woocommerce_email_order_meta_fields to add the fields to order emails:

add_filter( woocommerce_email_order_meta_fields, prefix_add_delivery_meta_to_emails, 10, 3 )
function prefix_add_delivery_meta_to_emails( fields, sent_to_admin, order ) {
    fields[delivery_timeslot] = array(
        label => Delivery timeslot,
        value => get_post_meta( order->get_id(), _delivery_timeslot, true ),
    )
    fields[delivery_instructions] = array(
        label => Delivery instructions,
        value => get_post_meta( order->get_id(), _billing_delivery_instructions, true ),
    )
    return fields
}

Show custom fields on thank-you and order view pages

add_action( woocommerce_thankyou, prefix_show_delivery_info_on_thankyou, 20 )
add_action( woocommerce_view_order, prefix_show_delivery_info_on_order_view, 20 )
function prefix_show_delivery_info_on_thankyou( order_id ) {
    prefix_output_delivery_info( order_id )
}
function prefix_show_delivery_info_on_order_view( order_id ) {
    prefix_output_delivery_info( order_id )
}
function prefix_output_delivery_info( order_id ) {
    timeslot = get_post_meta( order_id, _delivery_timeslot, true )
    instructions = get_post_meta( order_id, _billing_delivery_instructions, true )
    if ( timeslot ) {
        echo ltpgtltstronggtPreferred delivery:lt/stronggt  . esc_html( timeslot ) . lt/pgt
    }
    if ( instructions ) {
        echo ltpgtltstronggtDelivery notes:lt/stronggt  . esc_html( instructions ) . lt/pgt
    }
}

Populate default values (e.g., from user meta)

If the user is logged in, you can set field defaults using the filter. This helps pre-fill values from previous orders or user meta.

add_filter( woocommerce_checkout_fields, prefix_prefill_from_user_meta )
function prefix_prefill_from_user_meta( fields ) {
    if ( is_user_logged_in() ) {
        user_id = get_current_user_id()
        prev_timeslot = get_user_meta( user_id, preferred_delivery_timeslot, true )
        if ( prev_timeslot ) {
            fields[order][delivery_timeslot][default] = prev_timeslot
        }
    }
    return fields
}

Re-ordering fields and priority

You can move fields by changing the priority (or by reconstructing the array order). Lower numbers appear first. Example: move order comments to appear earlier:

add_filter( woocommerce_checkout_fields, prefix_reorder_order_comments )
function prefix_reorder_order_comments( fields ) {
    if ( isset( fields[order][order_comments] ) ) {
        fields[order][order_comments][priority] = 50
    }
    return fields
}

Conditional fields and client-side behavior

When you need fields to appear/disappear depending on another field (for example, show delivery instructions only if deliver later is checked), you will add a field and then use JS to toggle visibility. Always still validate on the server.

Example: add a checkbox that toggles the visibility of an instructions text field, plus simple JS to show/hide it.

add_filter( woocommerce_checkout_fields, prefix_add_delivery_toggle_fields )
function prefix_add_delivery_toggle_fields( fields ) {
    fields[order][deliver_later] = array(
        type     => checkbox,
        label    => Deliver later,
        required => false,
        class    => array( form-row-wide ),
        priority => 110,
    )

    fields[order][delivery_instructions_conditional] = array(
        type        => text,
        label       => Delivery instructions (only if Deliver later is checked),
        required    => false,
        class       => array( form-row-wide, conditional-delivery-instructions ),
        priority    => 115,
    )

    return fields
}

Now include a small JS snippet to toggle the visibility. Insert this JS into your theme footer or enqueue a small script file. The example below demonstrates inline script in a plugin you should enqueue it properly.

jQuery( function(  ) {
    function toggleDeliveryInstructions() {
        var checked = ( #deliver_later ).is( :checked )
        if ( checked ) {
            ( .conditional-delivery-instructions ).closest( .form-row ).show()
        } else {
            ( .conditional-delivery-instructions ).closest( .form-row ).hide()
        }
    }
    // Initial toggle on page load
    toggleDeliveryInstructions()
    // Toggle on change
    ( document ).on( change, #deliver_later, toggleDeliveryInstructions )
})

Important: replace #deliver_later selector with the actual field ID rendered in your checkout. WooCommerce typically outputs field IDs like deliver_later (it uses the array key), but confirm in the page HTML.

Field types: quick reference and examples

  • text — simple single-line input
  • textarea — multi-line text
  • select — dropdown with options
  • radio — multiple choice radio buttons
  • checkbox — tickbox (boolean)
  • password — password input (rare on checkout)
  • date — if you use a date picker, ensure the date input is supported by your theme or add custom JS

Example: textarea field

add_filter( woocommerce_checkout_fields, prefix_add_order_notes_textarea )
function prefix_add_order_notes_textarea( fields ) {
    fields[order][customer_notes] = array(
        type        => textarea,
        label       => Extra notes,
        placeholder => Anything else we should know?,
        required    => false,
        class       => array( form-row-wide ),
        priority    => 210,
    )
    return fields
}

Tips for compatibility and best practices

  1. Use sanitization functions when saving data: sanitize_text_field(), esc_textarea(), sanitize_email() etc.
  2. Escape output with esc_html(), esc_attr() or esc_textarea() when printing values.
  3. Do not rely only on JS for validation — always validate server-side using woocommerce_checkout_process.
  4. When adding fields that affect price or shipping, recalculate on the server and adjust order totals appropriately.
  5. Test with guest checkout and logged-in users test with address fields and different payment gateways.
  6. Keep the field keys unique and use a consistent prefix for meta keys (e.g., _myplugin_fieldname) to avoid collisions.
  7. Prefer hooks and filters rather than editing WooCommerce templates its future-proof and upgrade-safe.

Advanced topics

Conditional server-side modifications

You may want to modify the checkout fields in PHP based on cart contents, shipping method, or payment gateway. Because the checkout is built early, you can check conditions inside the filter callback. Example: add a field only when a specific product ID is in the cart:

add_filter( woocommerce_checkout_fields, prefix_add_if_product_in_cart )
function prefix_add_if_product_in_cart( fields ) {
    target_product_id = 123 // set your product ID
    found = false
    foreach ( WC()->cart->get_cart() as cart_item ) {
        if ( cart_item[product_id] == target_product_id ) {
            found = true
            break
        }
    }
    if ( found ) {
        fields[order][special_instructions_for_product] = array(
            type     => text,
            label    => Instructions for product 123,
            required => false,
            priority => 205,
        )
    }
    return fields
}

Custom field types and rendering

If you need a field type that WooCommerce does not support out-of-the-box, you can use the woocommerce_form_field function to render a custom field and use hooks such as woocommerce_after_checkout_billing_form to output custom HTML. However, when possible, reuse the built-in types for better compatibility with themes and styling.

Common pitfalls

  • Expecting field keys to always have certain IDs — always inspect the generated HTML to target selectors correctly for JS.
  • Using unchecked superglobals: always wrap _POST access with isset() and use wp_unslash() before sanitization.
  • Forcing required fields with JS only — users with JS disabled will skip validation unless you validate server-side.
  • Storing sensitive data without encryption — do not store unneeded sensitive data in post meta.

Checklist before deploying to production

  1. Test with multiple payment gateways and shipping methods.
  2. Test with responsive mobile layouts and different themes.
  3. Confirm data is saved to order meta and displayed in admin and emails.
  4. Ensure translations: wrap user-facing strings with __() or _e() and load your text domain.
  5. Backup your site and test on staging before real deployment.

Quick reference code snippets

Remove field:

add_filter( woocommerce_checkout_fields, function( fields ) {
    unset( fields[billing][billing_company] )
    return fields
} )

Save posted checkout value to order meta (sanitized):

add_action( woocommerce_checkout_update_order_meta, function( order_id ) {
    if ( ! empty( _POST[my_custom_field] ) ) {
        update_post_meta( order_id, _my_custom_field, sanitize_text_field( wp_unslash( _POST[my_custom_field] ) ) )
    }
} )

Display saved meta in admin order:

add_action( woocommerce_admin_order_data_after_billing_address, function( order ) {
    val = get_post_meta( order->get_id(), _my_custom_field, true )
    if ( val ) {
        echo ltpgtltstronggtMy custom field:lt/stronggt  . esc_html( val ) . lt/pgt
    }
} )

Summary

Customizing WooCommerce checkout fields using hooks is a robust and upgrade-safe approach. Use the woocommerce_checkout_fields filter to add/modify/remove fields, validate with woocommerce_checkout_process, save with woocommerce_checkout_update_order_meta, and display values where needed (admin, emails, thank you page). Always sanitize and escape, validate server-side, and test thoroughly across different scenarios and themes.

Useful links



Acepto donaciones de BAT's mediante el navegador Brave 🙂



Leave a Reply

Your email address will not be published. Required fields are marked *