WooCommerce Plus/Minus Quantity Buttons Not Working (Even After a Cart Update)

Quick Summary
- Attach one click handler to document and delegate it to .quantity .plus and .quantity .minus, so it keeps working after WooCommerce replaces the cart HTML.
- Change the value with the input's stepUp() and stepDown(), which respect the min, max and step WooCommerce prints, then trigger change so Update cart becomes active.
- Call preventDefault() so buttons without type="button" don't submit the form; the Cart block has its own buttons and isn't affected.
WooCommerce doesn’t add plus and minus buttons to the quantity field; they come from your theme or your own markup. When they do nothing, or work until the cart updates and then stop, the fix is the same: attach one delegated click handler to document, so it keeps working when WooCommerce replaces the cart HTML, and let the input’s own stepUp() and stepDown() change the value within its min, max and step.
Add the code to your theme’s JavaScript file (for example a custom.js file in your child theme that you enqueue with jquery as a dependency).
Make the plus and minus buttons work
jQuery(function ($) {
$(document).on('click', '.quantity .plus, .quantity .minus', function (e) {
e.preventDefault();
var input = $(this).closest('.quantity').find('input.qty').get(0);
if (!input || input.type !== 'number' || input.readOnly) {
return;
}
var oldValue = input.value;
if ($(this).hasClass('plus')) {
input.stepUp();
} else {
input.stepDown();
}
if (input.value !== oldValue) {
$(input).trigger('change');
}
});
});jQuery(function ($) { ... })runs the code when the page is ready and makes$available. WordPress loads jQuery in no-conflict mode, so a bare$outside the wrapper causes TypeError: $ is not a function.- The code attaches the handler to
documentand delegates it to.plusand.minusinside any.quantitybox. It finds the input withclosest('.quantity').find('input.qty'), so the buttons can sit before or after the input. e.preventDefault()matters when the buttons are<button>elements withouttype="button". A button inside a form submits it by default, which reloads the page or adds the product to the cart instead of changing the number.stepUp()andstepDown()are native methods of number inputs. They respect themin,maxandstepattributes WooCommerce prints, so the quantity stops at the product’s minimum (1 on a product page, 0 in the cart, where 0 removes the item) and at its stock limit.- If the input is hidden (a product sold individually) or read-only, nothing happens, because the quantity can’t change.
.trigger('change')tells WooCommerce the value changed, as if the customer had typed it. On the cart page, WooCommerce’s cart script listens for that event and enables the Update cart button.
Why the buttons stop working after the cart updates
When the customer clicks Update cart or removes an item, WooCommerce reloads the cart over AJAX and replaces the whole .woocommerce-cart-form with new HTML. Any handler bound directly to the old elements disappears with them. That is what happened with code like this:
$('.quantity').on('click', '.plus', function () {
// ...
});It is attached to the .quantity elements that exist on page load. After the first AJAX update those elements are gone, and the new ones have no handler, so the buttons do nothing.
An older fix was to attach the handlers again inside jQuery(document).ajaxComplete(), calling .off() first so one click didn’t trigger the handler several times. That works, but it re-runs after every AJAX request on the page. A handler delegated from document, as above, never needs rebinding: document is never replaced, and jQuery checks the clicked element against the selector at click time. You also don’t need your own code to enable the Update cart button; triggering change is enough.
If you need to run other code after WooCommerce refreshes the cart, it triggers the updated_wc_div event on document.body once the new cart HTML is in place:
jQuery(function ($) {
$(document.body).on('updated_wc_div', function () {
// The new cart HTML is in the page now.
});
});Adding the buttons yourself
If your theme has no plus and minus buttons, you can print them with the two hooks in WooCommerce’s quantity template, woocommerce_before_quantity_input_field and woocommerce_after_quantity_input_field. They run inside the .quantity wrapper, right before and after the input. Add this to your child theme’s functions.php:
add_action( 'woocommerce_before_quantity_input_field', function () {
echo '<button type="button" class="minus" aria-label="Decrease quantity">-</button>';
} );
add_action( 'woocommerce_after_quantity_input_field', function () {
echo '<button type="button" class="plus" aria-label="Increase quantity">+</button>';
} );The buttons then work with the JavaScript above. They are added wherever the classic quantity template is used, including for products sold individually, where the input is hidden; hide the buttons there with CSS if your theme shows them.
Things to check
- This is for the classic cart (the
[woocommerce_cart]shortcode) and classic product templates. The Cart block and the Add to Cart with Options block have their own plus and minus buttons, and this code doesn’t affect them. - If your theme or a plugin already binds its own plus and minus handlers, each click can change the quantity twice. Remove one of them.
- Your buttons need the classes
plusandminusand must be inside the.quantitywrapper. Change the selectors if your markup uses other names. - To gray out the minus button when the quantity is at 1, see disabling the minus button at quantity 1.
- After changing a theme JavaScript file, clear any page or asset cache so the browser loads the new version.
If your cart’s quantity controls or other interactive parts need fixing or rebuilding, see my WooCommerce development services.
Related articles
- running JavaScript again after every Ajax request: the ajaxComplete technique in another setting
- disabling the minus button at quantity 1: keep customers from going below one item on product pages
- fixing TypeError: $ is not a function: why jQuery code in WordPress needs a wrapper
