Skip to content
ThemesIonic — home
WooCommerce

Changing the WooCommerce Add to Cart Button Text

There are two add to cart filters, not one, and the label you are staring at is usually produced by the other one. Knowing which button belongs to which hook turns a frustrating afternoon into a four-line snippet.

4 min read intermediate

The filter name tells you almost nothing about which button it controls, and that is the whole problem. woocommerce_product_add_to_cart_text governs the button in the shop loop — category archives, related products, any product grid. The button on the single product page is a different filter with a different name. Most "my snippet does not work" reports are actually a snippet working perfectly on a button the author was not looking at.

Two filters, two buttons

Filter Where it applies Passed
woocommerce_product_add_to_cart_text Shop, category and tag archives, related and upsell grids The label and the product object
woocommerce_product_single_add_to_cart_text The button on the single product page The label and the product object

Both are applied inside the product object's own methods, which is why the product is handed to you as the second argument. That argument is the point of the whole exercise — a filter that ignores it and returns one string unconditionally will rename buttons that should never have said the same thing.

To receive it, declare two arguments when hooking. Omitting the argument count is the second most common failure, and it produces a PHP warning rather than a clean error, so it is easy to miss.

add_filter( 'woocommerce_product_add_to_cart_text', 'ti_loop_button_label', 10, 2 );

function ti_loop_button_label( $text, $product ) {
	if ( 'simple' === $product->get_type() && $product->is_purchasable() && $product->is_in_stock() ) {
		return __( 'Buy now', 'your-textdomain' );
	}

	return $text;
}

Returning $text unchanged in every other case is not defensive padding. It is what keeps the defaults intact for the product types you did not think about.

The defaults are not arbitrary

WooCommerce varies the loop label by product type because the button does genuinely different things:

  • Simple, purchasable, in stock adds straight to the cart without leaving the page.
  • Variable cannot add anything yet, so the label reflects that the click navigates to the product page to choose options — the model described in product variations.
  • Grouped sends the shopper to a page listing child products.
  • External or affiliate products carry their own button text field on the product itself, so the string you are overriding may have come from the editor rather than from a translation.
  • Out of stock falls back to a read-more style link, because there is nothing to add.

Overriding all of these with "Buy now" teaches shoppers that the same control sometimes buys and sometimes navigates. If you are changing labels for conversion reasons, change the simple in-stock case and leave the rest — and make the two states visually distinct, per product card design.

There is also a separate filter for the button's accessible description, the text that tells a screen reader which product the button belongs to. If you change the visible label to something that only makes sense alongside the product name, check that description still reads correctly.

When the filter genuinely does nothing

Work through these in order before rewriting the snippet:

  • The label is cached. A page cache serves the old HTML regardless of what PHP now returns. Clear it and retest in a private window, following clearing the WordPress cache.
  • The grid is rendered by blocks. A block-based product grid or a front end reading the Store API may produce the button from the block or the API payload rather than from the classic template path. Confirm where the string originates before assuming the hook is broken — the same distinction that governs customising the product archive.
  • The theme replaced the template. A theme shipping its own loop add-to-cart template can output a hard-coded string that never passes through the filter. Switch to a default theme for one page load to test.
  • Another callback runs later. A plugin filtering at a higher priority number wins. Raise yours only after confirming that is what is happening, rather than as a first move.
  • The product type is not what you assumed. A product with no price is not purchasable, so the simple-product branch never runs.

Where to put it

A child theme's functions.php or a small site-specific plugin. Not the parent theme, which the next update overwrites, and not a snippet stored only in a plugin you might one day deactivate while debugging something unrelated. The child theme route is covered in customising a WordPress theme.

If the goal is translation rather than rewording, stop and use translation files. A filter that returns an English string removes the button from the translation system entirely, and the bug surfaces months later on a language you added afterwards.

Change one label, then measure

Renaming the button is the cheapest experiment in a store and also the easiest to overrate. Change the simple in-stock label, leave every other type on its default, and give it long enough to see whether add-to-cart rate moved. If it did not, the button was never the obstacle — and the next place to look is what the card says above it.

Frequently asked

Because they are separate filters. The archive and grid buttons run through woocommerce_product_add_to_cart_text, while the button on the single product page runs through woocommerce_product_single_add_to_cart_text. Hook both if you want them to match.
That is the correct default, because a variable product cannot be added to the cart from a listing until options are chosen. Your filter is running, but if you return the same label for every type you will end up promising one-click purchase on a product that navigates instead.
No. A filter hard-codes one language into the template layer and bypasses the translation files entirely, so a multilingual site will show the same string to everyone. Translate the original string through your translation tooling instead.

Related guides