Hi Shopify team,
I found a mismatch between the documented onInput / onChange contract on s-number-field and what happens when controls="stepper" is used.
Environment
- Surface: Customer Account UI Extension
- Target:
customer-account.order-status.cart-line-item.render-after @shopify/ui-extensions:2026.4.3- Extension
api_version:2026-04 - Runtime: customer account web extension
What the docs say
s-number-field documents both input and change events.
The Polaris web components event guide says:
onInput: fires on every keystroke or value change. Use this for real-time updates.onChange: fires when the value is committed (e.g. blur). Use this after the user finishes.
controls="stepper" is a documented way to change the value incrementally (quantity selectors, etc.). Each stepper click is a value change, so onInput should fire on every increment/decrement.
What I see
Manual typing matches the docs:
onInputfires on every keystrokeonChangefires later, when the value is committed
Stepper controls do not:
- Clicking
+/-does not fireonInputcontinuously as the value changes onInputfires once, with the same timing asonChange
So for stepper interactions, onInput is effectively onChange. That breaks live quantity UIs (live totals, min-quantity messaging, enabling a button as the value updates, etc.).
Minimal repro
import { useState } from "preact/hooks";
function Extension() {
const [liveValue, setLiveValue] = useState("1");
const [committedValue, setCommittedValue] = useState("1");
return (
<s-stack gap="base">
<s-number-field
label="Quantity"
controls="stepper"
min={1}
step={1}
value={liveValue}
onInput={(event) => {
const value = event.currentTarget.value;
console.log("onInput", value);
setLiveValue(value);
}}
onChange={(event) => {
const value = event.currentTarget.value;
console.log("onChange", value);
setCommittedValue(value);
}}
/>
<s-text>onInput (live): {liveValue}</s-text>
<s-text>onChange (committed): {committedValue}</s-text>
</s-stack>
);
}
Steps
- Type
12into the field →onInputlogs twice (1, then12); live text updates on each keystroke. - Click stepper
+three times (or hold / click repeatedly) →onInputdoes not fire per click. Both handlers fire once, as if the value were committed.
Expected behavior
Stepper clicks should emit input on every increment/decrement, same as a keystroke.
change should still mean “committed” (blur / finished interaction).
Actual behavior
With controls="stepper", onInput is not continuous. It fires once and matches onChange.
Thanks.