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
[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.
| Capability | Classic shortcode checkout | Checkout block |
|---|---|---|
| How it renders | PHP template, hooks fire server-side in order | JavaScript front end over the Store API |
| Adding a field | woocommerce_checkout_fields filter, or echo HTML on a hook such as woocommerce_after_order_notes | woocommerce_register_additional_checkout_field(), limited to text, select, checkbox and date |
| Changing the layout | Template overrides in a child theme, plus CSS | Move inner blocks in the editor; toggles for Company, Address line 2 and Phone, each Optional or Required |
| Local Pickup (block version) | Not available at all | Required for it. Own Pickup Method step, tax calculated from the pickup location address |
| Express payments | Gateway-dependent, usually bolted above the form | Express Checkout inner block, shown only when an active gateway supports it |
| Third-party extensions | Effectively everything written in the last decade | Only extensions integrated against the Store API. Others render nothing, silently |
| Built-in presentation options | Your theme’s problem | Show form step numbers, dark mode inputs, removable Coupon Form, Return to Cart toggle |
| Order summary layout | Whatever the theme does | Two columns with a 360px summary sidebar at a container width of 920px and up, stacked below that |
| Where new work lands | Kept working for stores that need it | Every new checkout feature WooCommerce ships |
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 hook | Replacement in the blocks |
|---|---|
woocommerce_checkout_order_processed | woocommerce_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_step | Change quantity_limits through woocommerce_store_api_product_quantity_{$value_type} |
woocommerce_cart_item_thumbnail, woocommerce_product_get_image | woocommerce_store_api_cart_item_images |
woocommerce_cart_item_name, woocommerce_cart_item_remove_link | The itemName and showRemoveItemLink checkout filters, which are JavaScript, not PHP |
woocommerce_before_cart_totals, woocommerce_after_cart_totals, woocommerce_cart_actions | The ExperimentalOrderMeta slot and fill, again in JavaScript |
woocommerce_cart_totals_before_shipping, woocommerce_after_shipping_rate | ExperimentalOrderShippingPackages slot and fill |
woocommerce_cart_coupon | ExperimentalDiscountsMeta slot and fill |
woocommerce_order_button_text, woocommerce_order_button_html | No programmatic equivalent. Edit the text in the block editor |
woocommerce_enable_order_notes_field, woocommerce_checkout_show_terms | No 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_* family | Listed as not supported, with no alternative offered |
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
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:
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
The transform is built in and works both ways. Nothing is deleted, the page content is rewritten:
- 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.
- Open List View and select the Cart block itself, not an inner block.
- Click Transform, the leftmost button in the block toolbar, and choose Classic Shortcode. The block becomes a Classic Shortcode placeholder. Save.
- 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.
- 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
- Using the block-based checkout and Customizing the cart and checkout pages — the Transform steps, the compatibility warnings and the 8.3 default
- Checkout block documentation — inner block names, the address field toggles and the 920px two-column breakpoint
- Hook alternatives — every hook and replacement in the table above
- Additional checkout fields — the registration call, the four field types, the three locations and the meta key prefixes
- WooCommerce Blocks: Local Pickup — the settings path and every behavioural difference from the legacy method
- Cart and Checkout Blocks: becoming the default experience and FAQ: extending the cart and checkout blocks — the shortcode’s stated future, the
cart_checkout_blocksdeclaration, and the March 2025 comment on phase-out plans

Leave a Reply to This Post