WooCommerce refuses 0.5 in the quantity box for exactly one reason, and it is not the database: the quantity input renders with step="1", so the browser rejects the value before the form ever submits. Everything behind that input already tolerates decimals — wc_stock_amount() passes the submitted value through NumberUtil::normalize(), which preserves any numeric value rounded to six decimal places and only falls back to intval() for non-numeric junk, and the product lookup table stores stock_quantity as a double. One filter unblocks the field.
Knowing that is not the same as knowing whether you should use it, which is why we mapped the whole path while building By-Weight Pricing for WooCommerce — a plugin that deliberately does not put weight in the quantity field. Everything below was read from WooCommerce trunk in July 2026, against the 10.9.4 release line, with file and function names you can check in your own install.
Why does WooCommerce reject 0.5 in the quantity box?
Because templates/global/quantity-input.php prints a step attribute straight into the HTML, and its default value is 1. The template renders step whenever the input is not read-only, min unconditionally, and max only when the maximum is greater than zero — so a step of 1 becomes a browser-level constraint and Chrome or Firefox refuses the form with a "please enter a valid value" message. Nothing server-side has been consulted at that point.
That single attribute is the whole blocker. It is worth tracing what happens to a quantity once it gets past it, because the folklore about the rest of the path is out of date.
Input
The template prints step, min and max. Default step is 1, so the browser blocks 0.5 here.
Sanitize
Add-to-cart and cart updates call wc_stock_amount() on the raw request value.
Normalize
NumberUtil::normalize() keeps any numeric value, rounded to WC_ROUNDING_PRECISION — 6 decimals.
Store
Cart, order line items and the product lookup table all carry the decimal; stock_quantity is a double column.
Which filters actually control the quantity input in WooCommerce 10.x?
woocommerce_quantity_input_step, woocommerce_quantity_input_min and woocommerce_quantity_input_max — but where they are applied changed, and most published snippets describe the old path. In current core, wc_get_quantity_input_args() in includes/wc-template-functions.php only applies those three filters directly when no product object is available. When a product is passed — which is the normal case on a product page — it reads the values from the product instead:
if ( $product ) {
$defaults['min_value'] = $product->get_min_purchase_quantity();
$defaults['max_value'] = $product->get_max_purchase_quantity();
$defaults['step'] = $product->get_purchase_quantity_step();
$defaults['product_name'] = $product->get_title();
} else {
$defaults['min_value'] = apply_filters( 'woocommerce_quantity_input_min', 1, $product );
$defaults['max_value'] = apply_filters( 'woocommerce_quantity_input_max', -1, $product );
$defaults['step'] = apply_filters( 'woocommerce_quantity_input_step', 1, $product );
$defaults['product_name'] = '';
}
The filters still work, because those three product methods — added in WooCommerce 10.1.0 in includes/abstracts/abstract-wc-product.php — wrap the same hook names and then run the result through wc_stock_amount(). get_purchase_quantity_step(), for example, returns wc_stock_amount( apply_filters( 'woocommerce_quantity_input_step', 1, $this ) ). Two consequences follow: your filter callback now receives a real WC_Product object rather than possibly null, and whatever you return is normalised before it reaches the template.
One more detail from the same function is worth knowing before you write anything: get_max_purchase_quantity() returns 1 for any product marked sold individually, and wc_get_quantity_input_args() renders the input as type="hidden" when the minimum and maximum are equal and above zero. Sold-individually products are therefore immune to your step filter by construction.
What is the working snippet for decimal quantities?
Two filters, scoped to the products that actually need them. Add this to a small custom plugin or your child theme's functions.php:
// Allow 0.1 steps from 0.1 upward — scoped, not store-wide.
function my_weighted_product( $product ) {
return $product instanceof WC_Product
&& has_term( 'sold-by-weight', 'product_cat', $product->get_id() );
}
add_filter( 'woocommerce_quantity_input_step', function ( $step, $product ) {
return my_weighted_product( $product ) ? 0.1 : $step;
}, 10, 2 );
add_filter( 'woocommerce_quantity_input_min', function ( $min, $product ) {
return my_weighted_product( $product ) ? 0.1 : $min;
}, 10, 2 );
That is genuinely enough for the happy path in current core: the customer types 0.5, add-to-cart calls wc_stock_amount( '0.5' ), NumberUtil::normalize() returns 0.5, and the cart, the order line item and the stock decrement all carry it. The cart-update handler in includes/class-wc-form-handler.php strips the submitted value with preg_replace( '/[^0-9\.]/', '', … ) before normalising, so the decimal point survives that route too.
Scope it. Enabling fractional quantities store-wide on a catalog that also sells tickets, gift cards or bundles creates a class of order nobody wants to fulfil.
Does WooCommerce still force quantities to integers?
No, and this is the piece of received wisdom worth retiring. wc_stock_amount() in includes/wc-formatting-functions.php is now a single line:
function wc_stock_amount( $amount ) {
return NumberUtil::normalize( apply_filters( 'woocommerce_stock_amount', $amount ), intval( $amount ) );
}
NumberUtil::normalize() trims strings, checks is_numeric(), converts numeric strings with floatval(), and returns round( $value, WC_ROUNDING_PRECISION ) for anything that is not already an integer. WC_ROUNDING_PRECISION is defined as 6 in includes/class-woocommerce.php. The intval() you may have read about is only the fallback, used when a filter hands back something non-numeric. On the storage side, includes/class-wc-install.php declares stock_quantity as a double in the product lookup tables.
Which makes the woocommerce_stock_amount filter that every tutorial tells you to add — add_filter( 'woocommerce_stock_amount', 'floatval' ) — optional for the decimal to survive, and not harmless. Core exposes wc_is_stock_amount_integer(), defined as wc_stock_amount( 1 ) === 1, and uses it to build two input attributes:
'pattern' => apply_filters( 'woocommerce_quantity_input_pattern', wc_is_stock_amount_integer() ? '[0-9]*' : '' ),
'inputmode' => apply_filters( 'woocommerce_quantity_input_inputmode', wc_is_stock_amount_integer() ? 'numeric' : 'decimal' ),
Add the floatval filter and wc_stock_amount( 1 ) returns the float 1.0, which is not identical to the integer 1, so wc_is_stock_amount_integer() becomes false for the entire store. Every product's quantity box loses the digits-only pattern and switches to a decimal keypad on mobile — including the gift cards. There is no per-product version of that switch, because the function takes no product. If you want a decimal step on ten products, use the step filter and leave the stock filter alone.
Where does the snippet approach break down?
At six places, none of which is the input field. The snippet makes WooCommerce accept a fraction; it does not make the product priced by weight.
- No server-side step validation exists. The
stepattribute is a browser constraint. Core's add-to-cart handlers inclass-wc-form-handler.phpandclass-wc-ajax.phpsimply run the request value throughwc_stock_amount()— the wordmultiple_ofappears in current core only inside a docblock, not in any validation. A crafted request for0.137kg goes through. - Price stays a straight multiplication. Line total is price × quantity and nothing else: no per-kilogram brackets, no minimum-order economics, and no unit price rendered next to the selling price, which is a legal requirement for consumer sales in the EU and UK — see unit pricing laws for online stores.
- There are no per-product weight limits. Twenty grams of saffron and a 25 kg sack limit are the kind of constraint the quantity input has no vocabulary for, so you write and maintain that validation for the form handler, the AJAX route and the Store API yourself.
- Documents read like a spreadsheet error. A cart line showing "0.5 ×" carries no unit. Packing slips, invoices and order emails inherit the same bare number, and the support ticket asking whether that means half a kilo or half a packet arrives soon after.
- Quantity-based logic stops meaning anything. Sold-individually products cap at 1 by construction, and coupon minimums, free-shipping thresholds and quantity-based shipping rules all reason in units. A cart holding 0.25, 1.5 and 3 of different things breaks those rules quietly rather than loudly.
- Rounding has two different precisions. Quantities round to six decimals; money rounds to your currency's decimals. A per-kilogram price against an arbitrary weight produces values like €7.4925, and whether the cart total equals the sum of the displayed line items becomes something you audit rather than something you assume.
Why do weight plugins keep the quantity at 1 instead?
Because the cheapest way to avoid every integer assumption in core, themes and third-party integrations is to never put a fraction where an integer is expected. Dedicated by-weight plugins leave WooCommerce's quantity alone — usually locked at 1 — and store the chosen weight as cart item data, then compute the line price from it at woocommerce_before_calculate_totals.
Decimal quantity
- Quantity becomes 0.75; every integrator that reads quantity sees a fraction
- Validation is the browser's step attribute only
- Order line reads "0.75 ×" with no unit anywhere
- The store-wide inputmode flip hits products that must stay whole
Weight as item data
- Quantity stays 1; nothing downstream sees a fraction
- Min, max and step checked server-side per product
- Order line carries labelled meta, e.g. "Weight: 750 g"
- Only weighted products change behaviour
The trade-off runs the other way, and it is worth stating plainly: this architecture is purpose-built for weighted goods, not a general fractional-quantity switch. It also replaces the one-click add-to-cart flow on weighted products, because the customer has to choose a weight on the product page first. If what you actually need is 2.5 metres of cable, a weight plugin is the wrong tool — the fuller comparison of the options is in how to sell by weight in WooCommerce.
Which approach should you use?
Match the tool to how much of the pipeline you need, not to how the input looks.
| Your situation | Use |
|---|---|
| A handful of products, plain price × weight, and you maintain the code | The scoped step and min filters above |
| Customers choose a weight and you need per-product limits, per-kilogram display or wastage | A by-weight pricing plugin |
| Made-to-measure goods priced by area, volume or length | A measurement price calculator |
| Fixed packs, no chosen weight, but an EU or UK unit price is required | A unit-price display plugin |
The snippet is not a trap — it is a genuinely correct answer for a small, well-scoped catalog, and it costs nothing. It stops being the right answer the moment the number in that box needs to mean something to anyone other than the multiplication.
FAQ
Can the WooCommerce quantity field accept decimals?
Not by default — the quantity input is rendered with a step attribute of 1, so the browser rejects fractional values before the form submits. Filtering woocommerce_quantity_input_step and woocommerce_quantity_input_min to a fractional value enables decimal entry, but pricing tiers, per-product limits and unit display still need custom code or a plugin.
Does wc_stock_amount() convert quantities to integers?
No, and that advice is out of date. In current WooCommerce, wc_stock_amount() normalises through NumberUtil::normalize(), which preserves any numeric value rounded to WC_ROUNDING_PRECISION — defined as 6 — and only falls back to intval() when a filter returns something non-numeric. The product lookup tables store stock_quantity as a double column.
Do I still need the woocommerce_stock_amount floatval filter?
Not for the decimal to survive, and adding it has a store-wide side effect. Core derives the quantity input's pattern and inputmode attributes from wc_is_stock_amount_integer(), which is defined as wc_stock_amount( 1 ) === 1 — so filtering stock amounts through floatval flips every product in the catalog to a decimal keypad, not just the ones sold by weight.
Why do weight plugins use a separate weight field instead of decimal quantities?
Because keeping WooCommerce's quantity as an integer and storing the chosen weight as cart item data avoids every integer assumption in core, themes and third-party integrations at once. It also allows server-side minimum, maximum and step checks per product, per-kilogram pricing, and a properly labelled weight such as "750 g" on the cart line, the order and the emails.
