WooCommerce Block Checkout vs Shortcode: Which to Use

WooCommerce Block Checkout vs Shortcode: Which to Use

The WooCommerce block checkout vs shortcode decision is not a design question, it is a plugin compatibility question. The blocks have been the default for every store installed since WooCommerce 8.3 and they are where all new checkout development happens, but the classic PHP hooks your existing extensions and snippets were written against largely do not run inside them. So the answer is: switch when you can name every plugin and every line of custom code that touches your checkout, and not before.

Nobody is forcing the move. Asked directly in March 2025, WooCommerce core developer Mike Jolley said there were no plans to phase out the classic cart and checkout in core, and that merchants would get advance notice of any change. Treat that as a reprieve rather than a plan: the shortcode is kept working, not extended. What follows is what actually changes underneath, the hooks that stop firing, how to audit your own install, how to switch in both directions, and the cases where staying on the shortcode is the correct call.

Find out which one you are running first

Open your checkout page in the editor. One block called Checkout means you are on the blocks. A placeholder containing [woocommerce_checkout] means you are on the classic shortcode. On a block theme the path is Appearance → Editor → Pages → Checkout and then the edit icon; on a classic theme it is Pages → All Pages → Checkout. Stores set up before November 2023 are usually still on the shortcode no matter how current WooCommerce itself is, because an update never rewrites your page content.

What actually changes underneath

The classic checkout is a PHP template. WooCommerce renders it server-side, your plugins hook into it at documented points, print their HTML, and the whole form is submitted to a WooCommerce AJAX endpoint where more hooks fire in a predictable order. Every checkout customisation written between 2011 and about 2023 assumes that shape.

The Checkout block is a JavaScript application that reads and writes through the Store API. Cart contents, shipping rates, chosen payment method and the order itself all move over REST requests under /wc/store/v1/. Nothing is printed into a PHP template because there is no PHP template to print into, and the order is created by a Store API request rather than by a form post.

That single architectural change is the whole story. A plugin that echoes a field onto the classic template has nowhere to echo it. A plugin that validates on woocommerce_checkout_process is waiting for a request that never arrives. Worse, there is no error: the field simply is not there, and the store looks fine until someone asks why nobody is filling in the delivery instructions any more.

WooCommerce block checkout vs shortcode: the differences that matter

Ignore the marketing comparisons. These are the differences that change what you can do and what you have to rebuild. On a narrow screen the table stacks into one card per row.

CapabilityClassic shortcode checkoutCheckout block
How it rendersPHP template, hooks fire server-side in orderJavaScript front end over the Store API
Adding a fieldwoocommerce_checkout_fields filter, or echo HTML on a hook such as woocommerce_after_order_noteswoocommerce_register_additional_checkout_field(), limited to text, select, checkbox and date
Changing the layoutTemplate overrides in a child theme, plus CSSMove inner blocks in the editor; toggles for Company, Address line 2 and Phone, each Optional or Required
Local Pickup (block version)Not available at allRequired for it. Own Pickup Method step, tax calculated from the pickup location address
Express paymentsGateway-dependent, usually bolted above the formExpress Checkout inner block, shown only when an active gateway supports it
Third-party extensionsEffectively everything written in the last decadeOnly extensions integrated against the Store API. Others render nothing, silently
Built-in presentation optionsYour theme’s problemShow form step numbers, dark mode inputs, removable Coupon Form, Return to Cart toggle
Order summary layoutWhatever the theme doesTwo columns with a 360px summary sidebar at a container width of 920px and up, stacked below that
Where new work landsKept working for stores that need itEvery new checkout feature WooCommerce ships
Inner block names, settings and the 920px breakpoint are from WooCommerce’s Checkout block documentation, not measurements taken here.

The classic hooks that stop firing

WooCommerce publishes a hook-by-hook reference for this, and it is the most useful page in the whole blocks documentation because it is blunt about what was dropped. A sample of what a classic checkout customisation typically relies on, and what replaced it:

Classic hookReplacement in the blocks
woocommerce_checkout_order_processedwoocommerce_store_api_checkout_order_processed. The classic hook does not fire on Store API requests
woocommerce_cart_item_quantity, woocommerce_quantity_input_min, woocommerce_quantity_input_max, woocommerce_quantity_input_stepChange quantity_limits through woocommerce_store_api_product_quantity_{$value_type}
woocommerce_cart_item_thumbnail, woocommerce_product_get_imagewoocommerce_store_api_cart_item_images
woocommerce_cart_item_name, woocommerce_cart_item_remove_linkThe itemName and showRemoveItemLink checkout filters, which are JavaScript, not PHP
woocommerce_before_cart_totals, woocommerce_after_cart_totals, woocommerce_cart_actionsThe ExperimentalOrderMeta slot and fill, again in JavaScript
woocommerce_cart_totals_before_shipping, woocommerce_after_shipping_rateExperimentalOrderShippingPackages slot and fill
woocommerce_cart_couponExperimentalDiscountsMeta slot and fill
woocommerce_order_button_text, woocommerce_order_button_htmlNo programmatic equivalent. Edit the text in the block editor
woocommerce_enable_order_notes_field, woocommerce_checkout_show_termsNo filter. Remove the block in the editor instead
woocommerce_checkout_create_order, woocommerce_cart_subtotal, woocommerce_cart_total, woocommerce_local_pickup_methods, the woocommerce_form_field_* familyListed as not supported, with no alternative offered
Condensed from WooCommerce’s hook alternatives reference. That page is long and worth reading in full before you estimate a migration.

Read the last row twice. Those are not hooks with a newer equivalent, they are capabilities that currently have no replacement. If your store’s checkout depends on one of them, the migration is not a configuration change, it is a rewrite with an open question in the middle of it.

Note also what the replacements are made of. Half of them are JavaScript slot-and-fill components with Experimental in the name. A PHP developer who maintained a checkout plugin for ten years now needs a build step, a React component and a tolerance for APIs that are explicitly labelled as not yet stable. That is why so many small extensions still have not made the jump, and it is a fact about your timeline rather than a complaint about the design.

Audit your plugins before you touch the page

WooCommerce does part of this for you. Select the Checkout block or any of its inner blocks in the editor and look at the settings sidebar: if an active extension has declared itself incompatible, you get a warning there plus a one-click button to switch to the classic checkout. Select the Payment Options inner block for the same treatment on gateways. If no active gateway is compatible, your customers see "There are no payment methods available" and your store takes no money, which is the one failure mode here that is loud.

That warning only appears when a developer has bothered to declare the incompatibility. Declaration is a single call against the feature slug cart_checkout_blocks:

<?php
// Goes in the plugin's main file, not in functions.php.
add_action( 'before_woocommerce_init', function () {
	if ( class_exists( \Automattic\WooCommerce\Utilities\FeaturesUtil::class ) ) {
		\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility(
			'cart_checkout_blocks',
			__FILE__,
			true // false to declare incompatibility.
		);
	}
} );

Silence is the common case and silence is not compatibility, it usually means nobody looked. So do your own pass. If you have SSH, this finds most of the classic-checkout code on the site in one command:

# Hooks a classic checkout customisation almost always uses.
grep -rn --include="*.php" \
  -e "woocommerce_checkout_fields" \
  -e "woocommerce_after_order_notes" \
  -e "woocommerce_review_order" \
  -e "woocommerce_checkout_process" \
  -e "woocommerce_checkout_update_order_meta" \
  -e "woocommerce_before_checkout_form" \
  wp-content/plugins/ wp-content/themes/

Every hit is a file to open, not a verdict. Plenty of plugins hook both paths and handle the blocks correctly elsewhere. What you are building is a shortlist: for each plugin on it, find a statement about Store API or block checkout support in its changelog or on its WordPress.org page. Vendors who have done the work say so prominently. If a plugin is silent and has had no release in two years, you have your answer.

Which categories to check first

Anything that puts a field on the checkout or reads one off it: delivery date and time-slot pickers, order-notes add-ons, gift messaging, EU and VAT field plugins, age verification, custom validation snippets, and conversion tools that inject upsells into the form. Disclosure: I build and maintain Delivery Manager for WooCommerce, which sits in exactly that category, so check it on the Checkout block sidebar the same way you would check Order Delivery Date for WooCommerce or anything else on your list. The sidebar on your own install is the only answer that counts.

If a field disappears after you switch and you cannot tell which plugin owned it, work it the way you would any conflict: disable half, test, repeat. Troubleshooting a WordPress plugin conflict step by step covers doing that on a store you cannot take offline.

Register your own fields the supported way

If the field you need is a plain input, you do not need a plugin or a React component. The Additional Checkout Fields API registers it in PHP and the block renders it:

<?php
// wp-content/plugins/wpfairs-store-snippets/checkout-fields.php
// Register on woocommerce_init. Earlier causes init and translation problems.
add_action( 'woocommerce_init', function () {
	woocommerce_register_additional_checkout_field(
		array(
			'id'       => 'wpfairs/drop-off-note',
			'label'    => 'Where should we leave it?',
			'location' => 'order',  // contact | address | order
			'type'     => 'text',   // text | select | checkbox | date
		)
	);
} );

The location value is the part worth getting right, because it decides where the value is stored as well as where it appears:

contact renders at the top with the email address and saves against the shopper’s account. Right for a phone number you want to keep; wrong for anything order-specific.
address appears in both the shipping and billing forms, with separate values for each, and saves to the customer and the order. Note the consequence: the customer is asked twice.
order renders as the last step by default, can be moved in the editor, and saves to the order only. This is where delivery instructions, gift messages and PO numbers belong.

Reading the value back on the order, WooCommerce prefers its own service over direct meta access:

<?php
// Supported route. Import CheckoutFields and Package per the WooCommerce
// additional-fields reference, then ask for one group at a time.
$fields = Package::container()->get( CheckoutFields::class );
$other  = $fields->get_all_fields_from_object( $order, 'other' );

// Direct meta works and is discouraged. Keys are prefixed by group:
// _wc_billing/ , _wc_shipping/ , _wc_other/
$note = $order->get_meta( '_wc_other/wpfairs/drop-off-note' );

Know the ceiling before you design around it. Four field types, no textarea and no file upload. Select fields take no default value and you cannot choose the placeholder when the field is required. Attributes are not supported on selects, and autofocus and disabled are deliberately dropped. Values for a logged-in customer only persist once an order is placed, so read from wc()->customer during checkout rather than from a freshly loaded customer object. If your requirement is a free-text box three lines tall, the API cannot do it today and that is a reason to wait.

Local Pickup only exists on the block checkout

This is the one feature that settles the argument for a lot of stores, because there is no way to get it on the shortcode. The block version of Local Pickup is configured at WooCommerce → Settings → Shipping under its own Local Pickup tab: tick to enable, optionally set a method title and a pickup price with its tax status, then add at least one pickup location with a name and address. Without a location it does not appear at checkout.

It behaves differently from the legacy pickup shipping method in ways that matter operationally:

  • It gets its own Pickup Method step, separate from shipped orders, and shows up before the customer has entered an address, because it does not collect a shipping address at all.
  • Tax is calculated from the selected pickup location’s address. The legacy method uses your store address. For a multi-location store in more than one tax jurisdiction, that difference is the whole point.
  • It cannot be restricted to a shipping zone. It is offered to everyone regardless of address, and if you need zone restriction you are back on the legacy method.
  • Multiple packages are combined into one for pickup, so a single selection applies to the whole order, and if any package cannot be collected the option is disabled entirely.
  • With "hide shipping costs until an address is entered" on, pickup stays visible but is never auto-selected. The customer has to choose it.

Enabling both the block version and the legacy method at once produces a notice on the settings page, and rightly so: customers get two pickup options that behave differently. Pick one. If what you actually need is pickup slots, per-location stock or a delivery date, none of that is in core either way, and WooCommerce local delivery and pickup plugins compared covers what each layer costs.

How to switch, in both directions

Do this on staging and place a real test order

Not a cart test. An order, through your live gateway in test mode, with a coupon applied, a shipping method chosen and whatever custom field your store depends on. Then open the order in the admin and confirm every value you expected is on it. Checkout is the one page where a silent failure costs you money per visitor rather than per day, and the failure mode here is a missing field rather than an error message.

The transform is built in and works both ways. Nothing is deleted, the page content is rewritten:

  1. Open the Cart page. On a block theme go to Appearance → Editor → Pages, select Cart, click the edit icon. On a classic theme go to Pages → All Pages and edit it directly.
  2. Open List View and select the Cart block itself, not an inner block.
  3. Click Transform, the leftmost button in the block toolbar, and choose Classic Shortcode. The block becomes a Classic Shortcode placeholder. Save.
  4. Repeat on the Checkout page. Do both: the Cart and Checkout blocks are designed to work together and a half-migrated pair produces inconsistent shipping and coupon behaviour.
  5. To go the other way, the same Transform button converts the Classic Shortcode placeholder back into the blocks.

Two practical notes. If you had hand-edited the Cart or Checkout page content over the years, the transform can leave you with a block validation warning rather than a clean swap; fixing "this block contains unexpected or invalid content" covers recovering that without losing the page. And run your post-change checks properly, because a checkout swap deserves the same discipline as a core update: how to test your WordPress site after a major update has a checklist you can work through.

What switching does not fix

Worth being clear, because "move to block checkout" gets recommended as a fix for things it has nothing to do with.

Checkout caching. The cart and checkout pages still must not be served from a full-page cache, and Store API responses carry per-customer data that must not be cached either. Moving to blocks does not make the checkout cacheable, it changes where the uncacheable part lives. WooCommerce caching plugins: what breaks and what works covers which plugins set store exclusions for you and which need them written by hand.

Order storage. Block checkout and High-Performance Order Storage are separate features with separate compatibility declarations, cart_checkout_blocks and custom_order_tables. A plugin can support one and not the other. If you are planning both changes, do them one at a time with real trading days in between, and read WooCommerce HPOS migration: risks and real gains first, because that one is the harder of the two to reverse.

Order emails that do not arrive. Nothing about the checkout page affects deliverability. If confirmations are landing in spam, that is PHP mail() and your sending domain, and it needs authenticated SMTP with SPF, DKIM and DMARC. WooCommerce SMTP plugins compared for order emails covers the options.

A checkout that looks wrong. The blocks give you more design control than the shortcode, but they take their colours, type and spacing from your theme. If the result looks off, the fix is in your theme’s global styles rather than in the block settings, and customizing a block theme with theme.json or a child theme covers where those changes should live so an update does not undo them.

Should you switch?

  • Store installed since late 2023, no custom checkout code. You are already on the blocks and there is nothing to decide. Spend the time on your product pages instead.
  • Stock checkout, plugins all from WooCommerce.com or actively maintained. Switch. Audit, staging, one test order per payment method, go. This is the clearest yes on the page.
  • You need multi-location pickup with location-based tax. Switch, because the block Local Pickup is the only way to get it and no amount of configuration gets it onto the shortcode.
  • You want Apple Pay or Google Pay buttons in the flow. Lean yes. The Express Checkout block is the supported place for them, as long as your gateway declares support.
  • One essential plugin declares itself incompatible. Do not switch yet. Ask the vendor for a date. No date means you are costing a replacement, and that is the decision in front of you rather than the checkout type.
  • Bespoke checkout code and no developer on hand. Stay, and budget for the rewrite rather than attempting the switch. The hooks table above is your scoping document.
  • You need a field type the API does not have. Stay for now. A multi-line textarea or a file upload is not available through the supported API, and faking it in JavaScript on your checkout is a maintenance commitment, not a workaround.
  • No staging site. Fix that first. Everything on this page is reversible in one click, right up until the gateway you did not test turns out to be the incompatible one.

Whichever way you go, write down the date and the reason. The expensive version of this is not a failed migration, it is a store that switched in a hurry, lost a field nobody noticed, and spent eight months taking orders with no delivery instructions on them.

Frequently asked questions

Is the WooCommerce checkout shortcode deprecated?

Not deprecated and not scheduled for removal. WooCommerce has said the cart and checkout shortcodes stay in core for existing stores with customised flows and for new stores whose needs the blocks do not yet cover, and in March 2025 core developer Mike Jolley said there were no plans to phase them out, with advance notice promised for any change. What has changed is the default: since WooCommerce 8.3 new installs get the blocks. Treat the shortcode as supported but frozen.

How do I switch back from block checkout to the shortcode?

Edit the Checkout page, open List View, select the Checkout block, click Transform in the block toolbar and choose Classic Shortcode, then save. Do the same on the Cart page, because the two blocks are built to work as a pair. On a block theme the pages are under Appearance → Editor → Pages; on a classic theme they are under Pages → All Pages. There is also a one-click switch in the settings sidebar when WooCommerce has detected an incompatible extension or payment gateway.

Why is my custom checkout field missing on the block checkout?

Because it was added through a classic hook such as woocommerce_checkout_fields or printed onto the classic template, and the blocks render a JavaScript form over the Store API instead. There is no PHP template for that code to touch, so the field is absent with no warning. The supported replacement is woocommerce_register_additional_checkout_field(), which covers text, select, checkbox and date fields. If your field is a textarea or a file upload, the API does not support it yet.

Why do my payment methods not show on block checkout?

Incompatible gateways may not appear as options at all, and if none of your active gateways is compatible the customer sees "There are no payment methods available". Select the Payment Options inner block in the editor and check its settings sidebar, which lists your active payment integrations and offers a one-click switch back to the classic checkout. Fix it by updating the gateway plugin, replacing it, or reverting the checkout until the vendor ships support.

Is the block checkout faster than the shortcode checkout?

WooCommerce has not published a benchmark comparing the two, and anyone quoting you a conversion-rate figure for the switch is quoting a vendor’s marketing. The architectures differ rather than one simply beating the other: the blocks ship a JavaScript bundle and fetch cart state over the Store API, while the shortcode renders server-side and does a full page load between steps. On a slow phone the block checkout has more script to execute; on a slow server the shortcode has more PHP to run. Measure your own checkout rather than trusting either claim.

Can I use block checkout with a classic (non-block) theme?

Yes. The Cart and Checkout blocks are page content, not templates, so they work on a classic theme too; you edit them under Pages → All Pages rather than in the Site Editor. What you lose is the template-level control a block theme gives you over the surrounding header, footer and page width. Expect some CSS work, because a classic theme’s stylesheet was written for the old checkout markup and may style elements the blocks no longer output.

Do I have to migrate to block checkout before enabling HPOS?

No, they are independent. Block checkout changes how the checkout page is rendered and how the order is submitted; High-Performance Order Storage changes which database tables orders live in. A plugin declares compatibility with each separately, under cart_checkout_blocks and custom_order_tables, and supporting one implies nothing about the other. Do them as two separate projects with normal trading in between so that when something breaks you know which change caused it.

Sources

Leave a Reply to This Post