WooCommerce: Variable Product shows error at Checkout after successful AJAX Add to Cart

I’m working on a custom AJAX-based Add to Cart implementation in WooCommerce. It works perfectly for simple products: the product is added via AJAX, a cart counter updates, and a slideout cart opens.

Here’s the simplified version of the code I’m using:

jQuery(function ($) {
  $('body').on('submit', 'form.cart', function (e) {
    e.preventDefault();

    const form = $(this);
    const button = form.find('button[type="submit"]');
    const svg = button.find('#cartsvg');
    const loader = button.find('.cartloader');
    const formData = new FormData(form[0]);

    if (!formData.has('add-to-cart')) {
      const addToCartVal = form.find('[name=add-to-cart]').val();
      formData.append('add-to-cart', addToCartVal);
    }

    const ajaxUrl = wc_add_to_cart_params.wc_ajax_url.toString().replace('%%endpoint%%', 'add_to_cart');

    if (typeof wc_add_to_cart_params !== 'undefined' && wc_add_to_cart_params.wc_ajax_nonce) {
      formData.append('_wpnonce', wc_add_to_cart_params.wc_ajax_nonce);
    }

    button.prop('disabled', true).addClass('loading');
    svg.addClass('loading-cart-icon');
    loader.addClass('visible-loader');

    $.ajax({
      url: ajaxUrl,
      type: 'POST',
      data: formData,
      processData: false,
      contentType: false,
      success: function (response) {
        if (response && response.fragments) {
          $.each(response.fragments, function (key, value) {
            $(key).replaceWith(value);
          });
        }

        $('#cart-items-count').text((parseInt($('#cart-items-count').text()) || 0) + 1);
        const productTitle = form.closest('.product, .single-product').find('.product_title').text().trim();
        $('#slideout-cart-content').text(productTitle);
        $('#ajax-cart-slideout, #ajax-slideout-overlay').removeClass('translate-x-full hidden').addClass('block');

        button.prop('disabled', false).removeClass('loading');
        svg.removeClass('loading-cart-icon');
        loader.removeClass('visible-loader');
      },
      error: function (xhr, status, error) {
        console.log('AJAX Error:', status, error);
        button.prop('disabled', false).removeClass('loading');
        svg.removeClass('loading-cart-icon');
        loader.removeClass('visible-loader');
      }
    });
  });

  function closeSlideout() {
    $('#ajax-cart-slideout').addClass('translate-x-full');
    $('#ajax-slideout-overlay').addClass('hidden');
  }

  $('#close-slideout').on('click', closeSlideout);
  $('#ajax-slideout-overlay').on('click', closeSlideout);
});

:white_check_mark: Observed Behavior:

Product (variable or simple) gets added to cart via AJAX.

Cart updates correctly.

Product variant (e.g., “Edition: Book”) is displayed correctly in mini cart and cart page.

:x: Issue:

At checkout, WooCommerce throws this error:

“Please choose product options by visiting the product page…”

Even though attribute_pa_edition, add-to-cart, product_id, and variation_id are all present in the FormData payload (and logged as such), WooCommerce seems to treat the cart item as incomplete or invalid.

:package: FormData logged before submission (example):

attribute_pa_edition: buch
add-to-cart: 68808
product_id: 68808
variation_id: 68809

Verified correct nonce and endpoint.

WooCommerce scripts wc-add-to-cart-variation.js and wc-add-to-cart.js are loaded.

formData logs expected keys and values.

When using the native WooCommerce form (without AJAX), everything works correctly.

What causes WooCommerce to reject a variable product at checkout after successful AJAX add to cart, despite correct variation data?

Is there something missing (e.g., in session data or cart item validation) that the native submit process handles, but my AJAX call does not?

You’re off to a great start with your custom AJAX “Add to Cart” implementation—it looks clean and efficient for simple products. However, if you’re planning to extend this to variable products or make it more robust across edge cases, here are some key enhancements and best practices:


Recommended Improvements & Fixes

1. Handle Quantity Properly

If your product has a quantity input, make sure you’re capturing that:

const qty = form.find('input.qty').val() || 1;
formData.set('quantity', qty);

This ensures the correct quantity is added to the cart.


2. Support for Variable Products

Right now, your code will likely fail silently or not add anything when used with variable products. To support them, collect variation data:

// For variable products
form.find('input[name^=attribute_], select[name^=attribute_]').each(function () {
  formData.append($(this).attr('name'), $(this).val());
});

// Also include variation ID if available
const variationId = form.find('input[name=variation_id]').val();
if (variationId) {
  formData.append('variation_id', variationId);
}

This ensures all necessary variation info is sent.


3. Better Cart Count Updating

Your current count update adds 1 every time, which might become inaccurate if:

  • quantity is more than 1
  • the cart already had items

Fix: Recalculate cart count based on actual fragment update or get it from the response.

If WooCommerce returns the cart count in fragments (e.g. #cart-items-count), rely on the server-rendered fragment.

Otherwise, use a custom endpoint to get the real cart count.


4. Improve UX with Notifications (optional)

Instead of just showing product title in the slideout, consider using a toast or modal with:

WooCommerce.add_notice('“' + productTitle + '” has been added to your cart.', 'success');

Or integrate with something like WC Notifier or your own notification system.


5. Error Feedback for Users

Currently, your error handler just logs to the console. Add a user-visible error, like:

error: function (xhr, status, error) {
  alert('There was an error adding the product to the cart. Please try again.');
  console.log('AJAX Error:', status, error);
  button.prop('disabled', false).removeClass('loading');
  svg.removeClass('loading-cart-icon');
  loader.removeClass('visible-loader');
}

6. Ensure wc_add_to_cart_params is Loaded

If the cart isn’t updating, make sure your theme or plugin enqueues wc-add-to-cart.js and that wc_add_to_cart_params is printed in the page source:

function enqueue_my_ajax_cart() {
    wp_enqueue_script('wc-add-to-cart'); // Optional but often needed
    wp_enqueue_script('my-custom-cart-js', get_template_directory_uri() . '/js/my-cart.js', ['jquery'], null, true);
    wp_localize_script('my-custom-cart-js', 'wc_add_to_cart_params', [
        'wc_ajax_url' => admin_url('admin-ajax.php') . '?action=%%endpoint%%',
        'wc_ajax_nonce' => wp_create_nonce('wc_ajax_nonce'),
    ]);
}
add_action('wp_enqueue_scripts', 'enqueue_my_ajax_cart');

Summary of Key Changes to JS

Here’s a condensed version of improvements to add after formData:

// Quantity
const qty = form.find('input.qty').val() || 1;
formData.set('quantity', qty);

// Variations
form.find('input[name^=attribute_], select[name^=attribute_]').each(function () {
  formData.append($(this).attr('name'), $(this).val());
});
const variationId = form.find('input[name=variation_id]').val();
if (variationId) {
  formData.append('variation_id', variationId);
}

Let me know if you want to expand this to grouped or subscription products, or need help writing the server-side PHP to support custom endpoints.