Skip to content
WooCommerce

How to Edit WooCommerce Checkout Page With Code (2026)

· · 12 min read
How to Edit WooCommerce Checkout Page

Editing the WooCommerce checkout page at the code level gives you complete control over fields, validation, layout, and behavior. Unlike no-code plugins, custom code edits are lightweight, performant, and survive theme changes when placed in the right location.

This guide covers the developer approach to editing WooCommerce checkout using PHP hooks, filters, and custom validation. For no-code and visual customization methods, see our companion guide on customizing the WooCommerce checkout page.

I’ve rebuilt checkout flows for stores selling everything from digital courses to physical furniture, and the pattern that keeps coming up is this: the merchants who touch core files or dump everything into functions.php are the ones filing support tickets six months later when an update wipes out their custom field. The ones who follow the hook-based approach below barely notice WooCommerce updates happen.

Where to Place Custom Checkout Code

Never edit WooCommerce core files or your parent theme’s functions.php directly. Use one of these approaches:

  • Custom plugin (recommended): Create a simple plugin file in wp-content/plugins/my-checkout-edits/. This survives theme changes and is easy to disable
  • Child theme functions.php: Works but ties your customizations to one specific theme
  • Code Snippets plugin: Quick for testing but harder to manage long-term
<?php
/**
 * Plugin Name: Custom Checkout Edits
 * Description: WooCommerce checkout field and validation customizations
 * Version: 1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) exit;

// Your checkout code goes here

One habit worth building early: version-control this plugin, even if it’s just a single file in a private git repo. Checkout logic tends to accumulate business rules over time (shipping cutoffs, regional tax quirks, loyalty program hooks) and losing that history because a client renamed a folder is a painful way to learn the lesson.

The woocommerce_checkout_fields Filter

This is the most important hook for editing checkout fields. It gives you access to all billing, shipping, and order fields before they render.

Remove Fields

add_filter( 'woocommerce_checkout_fields', 'custom_remove_checkout_fields' );
function custom_remove_checkout_fields( $fields ) {
    // Remove company name
    unset( $fields['billing']['billing_company'] );

    // Remove phone (use cautiously - some shipping providers need it)
    unset( $fields['billing']['billing_phone'] );

    // Remove order notes
    unset( $fields['order']['order_comments'] );

    // Remove Address Line 2
    unset( $fields['billing']['billing_address_2'] );
    unset( $fields['shipping']['shipping_address_2'] );

    return $fields;
}

A quick warning from experience: removing billing_phone looks harmless until your shipping carrier’s API rejects labels that don’t include a contact number, or your payment gateway’s fraud check flags orders with no phone on file. Test with your actual shipping and payment stack before you ship this to production, not just with a generic staging cart.

Make Fields Optional

add_filter( 'woocommerce_checkout_fields', 'custom_optional_fields' );
function custom_optional_fields( $fields ) {
    // Make phone optional instead of removing it
    $fields['billing']['billing_phone']['required'] = false;

    // Make company optional
    $fields['billing']['billing_company']['required'] = false;

    return $fields;
}

Reorder Fields

add_filter( 'woocommerce_checkout_fields', 'custom_reorder_fields' );
function custom_reorder_fields( $fields ) {
    // Move email to the top (lower priority = higher position)
    $fields['billing']['billing_email']['priority'] = 5;

    // Move phone after email
    $fields['billing']['billing_phone']['priority'] = 6;

    // Move first name after phone
    $fields['billing']['billing_first_name']['priority'] = 10;

    return $fields;
}

Add Custom Fields

add_filter( 'woocommerce_checkout_fields', 'custom_add_checkout_fields' );
function custom_add_checkout_fields( $fields ) {
    $fields['billing']['billing_delivery_date'] = array(
        'type'        => 'date',
        'label'       => 'Preferred Delivery Date',
        'required'    => false,
        'class'       => array( 'form-row-wide' ),
        'priority'    => 120,
    );

    $fields['order']['gift_message'] = array(
        'type'        => 'textarea',
        'label'       => 'Gift Message',
        'required'    => false,
        'class'       => array( 'form-row-wide' ),
        'placeholder' => 'Add a personal message (optional)',
    );

    return $fields;
}

// Save custom field values to order meta
add_action( 'woocommerce_checkout_update_order_meta', 'save_custom_checkout_fields' );
function save_custom_checkout_fields( $order_id ) {
    if ( ! empty( $_POST['billing_delivery_date'] ) ) {
        update_post_meta( $order_id, '_delivery_date',
            sanitize_text_field( $_POST['billing_delivery_date'] ) );
    }
    if ( ! empty( $_POST['gift_message'] ) ) {
        update_post_meta( $order_id, '_gift_message',
            sanitize_textarea_field( $_POST['gift_message'] ) );
    }
}

// Display custom fields in admin order view
add_action( 'woocommerce_admin_order_data_after_billing_address', 'display_custom_fields_admin' );
function display_custom_fields_admin( $order ) {
    $delivery = get_post_meta( $order->get_id(), '_delivery_date', true );
    $gift     = get_post_meta( $order->get_id(), '_gift_message', true );

    if ( $delivery ) {
        echo '<p><strong>Delivery Date:</strong> ' . esc_html( $delivery ) . '</p>';
    }
    if ( $gift ) {
        echo '<p><strong>Gift Message:</strong> ' . esc_html( $gift ) . '</p>';
    }
}

Field Types You Can Use

The type key in a custom field array isn’t limited to text and textarea. WooCommerce’s form-field template supports several built-in types, and knowing the full list saves you from writing a custom template unnecessarily:

TypeRenders asGood for
textStandard inputNames, tax IDs, reference numbers
emailEmail input with browser validationSecondary contact emails
telTelephone inputAlternate phone numbers
textareaMulti-line boxGift messages, delivery instructions
selectDropdown (needs an options array)Delivery windows, install preferences
radioRadio button groupBinary or short-list choices
checkboxSingle checkboxOpt-ins, terms acknowledgment
dateHTML5 date pickerDelivery dates, booking dates
passwordMasked inputAccount creation flows

For a select field, add an options array keyed by value:

$fields['order']['delivery_window'] = array(
    'type'     => 'select',
    'label'    => 'Preferred Delivery Window',
    'required' => false,
    'class'    => array( 'form-row-wide' ),
    'options'  => array(
        ''          => 'Select a window',
        'morning'   => '8am - 12pm',
        'afternoon' => '12pm - 5pm',
        'evening'   => '5pm - 9pm',
    ),
);

Custom Checkout Validation

WooCommerce provides hooks to add custom validation rules before an order is placed.

Basic Validation Example

add_action( 'woocommerce_checkout_process', 'custom_checkout_validation' );
function custom_checkout_validation() {
    // Require phone for physical products only
    if ( WC()->cart->needs_shipping() && empty( $_POST['billing_phone'] ) ) {
        wc_add_notice( 'Phone number is required for orders that need shipping.', 'error' );
    }

    // Validate delivery date is in the future
    if ( ! empty( $_POST['billing_delivery_date'] ) ) {
        $date = strtotime( sanitize_text_field( $_POST['billing_delivery_date'] ) );
        if ( $date && $date < strtotime( 'tomorrow' ) ) {
            wc_add_notice( 'Delivery date must be at least one day from now.', 'error' );
        }
    }
}

Conditional Validation Based on Cart Contents

add_action( 'woocommerce_checkout_process', 'validate_by_cart_contents' );
function validate_by_cart_contents() {
    $has_subscription = false;
    foreach ( WC()->cart->get_cart() as $item ) {
        $product = $item['data'];
        if ( $product->is_type( 'subscription' ) ) {
            $has_subscription = true;
            break;
        }
    }

    // Require account creation for subscription products
    if ( $has_subscription && ! is_user_logged_in() && empty( $_POST['createaccount'] ) ) {
        wc_add_notice( 'An account is required for subscription products. Please check "Create an account".', 'error' );
    }
}

Validating Postcode Against Shipping Zones

A pattern I use often for local-delivery stores: reject postcodes outside the serviceable area instead of letting the order through and cancelling it manually later.

add_action( 'woocommerce_checkout_process', 'restrict_delivery_postcodes' );
function restrict_delivery_postcodes() {
    $allowed_prefixes = array( '90210', '90211', '90212' );
    $postcode = isset( $_POST['billing_postcode'] ) ? sanitize_text_field( $_POST['billing_postcode'] ) : '';

    if ( WC()->cart->needs_shipping() && $postcode ) {
        $matched = false;
        foreach ( $allowed_prefixes as $prefix ) {
            if ( strpos( $postcode, $prefix ) === 0 ) {
                $matched = true;
                break;
            }
        }
        if ( ! $matched ) {
            wc_add_notice( 'Sorry, we currently only deliver to select ZIP codes. Contact us for other locations.', 'error' );
        }
    }
}

Checkout Action Hooks for Custom Content

WooCommerce provides action hooks at specific positions in the checkout form. Use these to inject content without modifying templates.

Available Checkout Hooks

HookPositionUse Case
woocommerce_before_checkout_formBefore the entire formSecurity notice, login prompt
woocommerce_checkout_before_customer_detailsBefore billing/shippingExpress checkout buttons
woocommerce_before_checkout_billing_formBefore billing fieldsBilling instructions
woocommerce_after_checkout_billing_formAfter billing fieldsAdditional billing info
woocommerce_before_checkout_shipping_formBefore shipping fieldsShipping notes
woocommerce_before_order_notesBefore order notesCustom sections
woocommerce_review_order_before_submitBefore Place Order buttonTerms, trust badges
woocommerce_review_order_after_submitAfter Place Order buttonGuarantee text

Adding Trust Badges Before the Payment Button

add_action( 'woocommerce_review_order_before_submit', 'add_trust_badges_checkout' );
function add_trust_badges_checkout() {
    echo '<div class="checkout-trust-badges" style="text-align:center; padding:15px 0; margin:10px 0; border-top:1px solid #eee;">';
    echo '<p style="font-size:13px; color:#666;">Secure checkout powered by 256-bit SSL encryption</p>';
    echo '<p style="font-size:12px; color:#888;">30-day money-back guarantee | Free shipping over $75</p>';
    echo '</div>';
}

Modifying the Checkout Template

For major layout changes, you can override WooCommerce templates in your child theme.

How to Override Checkout Templates

  1. Copy wp-content/plugins/woocommerce/templates/checkout/form-checkout.php
  2. Paste to wp-content/themes/your-child-theme/woocommerce/checkout/form-checkout.php
  3. Edit the copied file

Warning: Template overrides require maintenance. After every WooCommerce update, check if the original template changed and merge any differences. WooCommerce marks template version in the file header comment.

In practice, I keep a simple habit for this: before updating WooCommerce on any store with template overrides, I diff the plugin's shipped template against my overridden copy using a basic diff command. If nothing changed, the update is safe to run immediately. If something did change, I schedule twenty minutes to review it before touching production. That one habit has saved more broken checkouts than any other single practice on this list.

Disabling Fields for Digital Products

If you sell only digital products (downloads, licenses, courses), you can remove all shipping fields and simplify billing:

add_filter( 'woocommerce_checkout_fields', 'simplify_digital_checkout' );
function simplify_digital_checkout( $fields ) {
    // Only simplify if cart has no physical products
    if ( ! WC()->cart->needs_shipping() ) {
        // Remove all shipping fields
        unset( $fields['shipping'] );

        // Remove unnecessary billing fields for digital
        unset( $fields['billing']['billing_company'] );
        unset( $fields['billing']['billing_address_1'] );
        unset( $fields['billing']['billing_address_2'] );
        unset( $fields['billing']['billing_city'] );
        unset( $fields['billing']['billing_postcode'] );
        unset( $fields['billing']['billing_country'] );
        unset( $fields['billing']['billing_state'] );
        unset( $fields['billing']['billing_phone'] );
    }

    return $fields;
}

HPOS Compatibility Note

If your store uses HPOS (High-Performance Order Storage), replace update_post_meta() and get_post_meta() with WooCommerce's order object methods:

// Instead of: update_post_meta( $order_id, '_delivery_date', $value );
// Use:
$order = wc_get_order( $order_id );
$order->update_meta_data( '_delivery_date', $value );
$order->save();

// Instead of: get_post_meta( $order_id, '_delivery_date', true );
// Use:
$order = wc_get_order( $order_id );
$delivery = $order->get_meta( '_delivery_date' );

This ensures your code works with both the legacy post-based storage and the new custom order tables. For more on HPOS and performance, see our guide on speeding up WooCommerce.

One thing that trips up developers coming back to older tutorials: HPOS has been the default for new WooCommerce installs for a while now, but plenty of stores still run on legacy post storage because they never migrated. Don't assume either way, check under WooCommerce > Settings > Advanced > Features before you write meta-handling code, or better, use the order object methods above unconditionally. They work correctly regardless of which storage mode is active, so there's rarely a reason to branch your code based on it.

Block-Based Checkout: What Changes

Everything above targets the classic shortcode-based checkout (the one rendered by [woocommerce_checkout]). Most of the hooks and filters still fire correctly if your store uses the classic checkout, even on a recent WooCommerce version, WooCommerce kept it available specifically because so many stores depend on custom field code exactly like this.

If you've switched to the block-based checkout (the Checkout block added through the block editor), the legacy woocommerce_checkout_fields filter is not read by that renderer. Fields you add through the filter simply won't appear. Instead:

  • Additional checkout fields API: Register custom fields with woocommerce_register_additional_checkout_field(), which the Checkout block reads natively and renders with the correct styling automatically.
  • Store API extensions: For data that needs to flow through the block checkout's REST-based Store API, register an ExtendSchema instance so your custom data is included in the cart and checkout responses.
  • Block-compatible plugins: If you'd rather not write this integration by hand, plugins built specifically for block checkout (rather than the classic shortcode) handle the registration for you through an admin screen.

A quick way to check which checkout you're running: open the Checkout page in the block editor. If you see a purple "Checkout" block with nested inner blocks (Contact Information, Shipping Address, Payment Options), you're on the block checkout and need the newer API. If the page contains only the classic [woocommerce_checkout] shortcode, everything in this guide applies directly.

Testing Checkout Edits Safely

  • Always test on staging first: Never deploy checkout code directly to production
  • Test all payment gateways: Each gateway renders differently and may conflict with custom fields
  • Test guest and logged-in checkout: Some hooks behave differently for authenticated users
  • Test mobile devices: Custom fields must render correctly on small screens
  • Monitor after WooCommerce updates: Major updates can change hook behavior or add new fields
  • Test with an empty cart of one item and a full cart: Custom validation that references cart totals or line items can behave differently at the edges
  • Check the order-received (thank you) page: If your custom field data feeds into confirmation messaging, verify it displays correctly there too, not just in the admin order screen

I'd also add: keep a spreadsheet of every hook you've used and why. It sounds like overkill for a five-field checkout, but two years and three developers later, that spreadsheet is the only thing standing between "quick fix" and "spend an afternoon reverse-engineering why removing a field breaks tax calculation."

Debugging When a Checkout Edit Doesn't Show Up

Almost every "my custom field isn't showing" ticket I've debugged traces back to one of four causes, in roughly this order of likelihood:

  • Page caching: A full-page cache plugin served a stale version of the checkout page before your code ran. Clear the cache and, ideally, exclude the checkout and cart pages from page caching entirely, they should never be cached for a logged-in or cart-holding visitor anyway.
  • Wrong field array key: The woocommerce_checkout_fields filter is nested (billing, shipping, order, and on some setups account). Adding a field to the wrong top-level key means it silently does nothing.
  • Block checkout in use: As covered above, legacy field filters don't run at all on the block-based checkout. Confirm which checkout renderer is active before debugging further.
  • Priority conflicts with another plugin: If a payment or shipping plugin also hooks into woocommerce_checkout_fields at a later priority, it can overwrite your changes. Use add_filter( 'woocommerce_checkout_fields', 'your_function', 20 ) to run after most third-party plugins, which typically hook at the default priority of 10.

Query Monitor (a free debugging plugin) is worth installing on staging specifically for this kind of issue, it shows you every hook fired on the checkout page and in what order, which turns a guessing game into a five-minute lookup.

FAQ

What is the main hook for editing WooCommerce checkout fields?

The woocommerce_checkout_fields filter gives you access to all checkout fields (billing, shipping, order). You can add, remove, reorder, or modify any field through this single filter. This applies to the classic shortcode checkout; the block checkout uses a different registration API, covered above.

How do I save custom checkout field data?

Use the woocommerce_checkout_update_order_meta action hook. Access the posted data from $_POST, sanitize it, and save with update_post_meta() or the HPOS-compatible $order->update_meta_data().

Can I add validation to checkout without a plugin?

Yes. Hook into woocommerce_checkout_process and use wc_add_notice() with type error to block checkout when validation fails.

Will custom code break after WooCommerce updates?

Hooks and filters are stable across updates. Template overrides may need updating when WooCommerce changes the original template. Always test on staging after major WooCommerce updates.

Should I use hooks or template overrides?

Use hooks whenever possible. They are more maintainable and less likely to break on updates. Only override templates when you need to fundamentally change the checkout HTML structure.

How do I make checkout edits compatible with block checkout?

The block checkout uses a different rendering system. Legacy woocommerce_checkout_fields filters do not apply to block checkout. For block checkout customization, register fields with woocommerce_register_additional_checkout_field() and extend the Store API schema for any data that needs to persist through checkout.

Do I need a developer to make these changes, or can plugins do it?

If you're comfortable with basic PHP, the plugin approach in this guide is straightforward and free. If not, checkout customization plugins built for either the classic or block checkout (search for ones explicitly marked block-compatible if you're on the newer checkout) can achieve similar results through an admin UI, usually at the cost of one more plugin to keep updated.

Keeping Checkout Code Maintainable

Editing WooCommerce checkout with code gives you precise control that plugins cannot match. Start with the woocommerce_checkout_fields filter for field management on classic checkout, or the additional-fields API on block checkout, use action hooks for injecting content, and add custom validation to enforce business rules.

Keep your code in a custom plugin, test thoroughly on staging, and ensure HPOS compatibility for future-proof implementations. Checkout is the one page on your store where a bug directly costs revenue within minutes of shipping it, so the extra ten minutes spent testing on staging is never wasted time.

Related reading: Customize Checkout (No-Code Methods) | WooCommerce Custom Product Fields | WooCommerce Fashion Store Setup