Quantity — <mv-quantity>
E-commerce quantity picker, a thin variant of <mv-number-field>: minimum 1, automatically capped at stock, an “In stock” / “Only 3 left in stock” / “Out of stock” caption linked to the field, a message and shake when hitting the stock limit, a disabled sold-out state that re-enables on restock; pill and subtle variants for cart lines.
Category Forms Type Web Component (<mv-quantity>) Status stable Also installs number-field Keywords quantity, quantité, stepper, stock, out-of-stock, cart, e-commerce, form-associated
When to use
A product page or cart line needs a quantity picker capped at available stock Shoppers should see In stock, Only 3 left or Out of stock next to the quantity A compact pill or muted variant must fit inside cart rows
Avoid when
The number is not a purchase quantity, like a price, percentage or measurement → use Number Field instead
Get Quantity. Included in every plan: the Pro pack ships all 226 components with the CLI and the MCP server used below.
See plans
Install
node scripts/add.mjs quantity --out ./src/marvelous
AI agent with the Marvelous UI MCP server: install_components({ slugs: ["quantity"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/motion.js, components/quantity/quantity.js, components/quantity/quantity.css, components/number-field/number-field.js, components/number-field/number-field.css.
Usage
Canonical markup — start from it and customize with attributes, data-* and CSS variables:
<div id="quantity-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14rem),1fr));gap:1.25rem;width:min(100%,50rem);margin-inline:auto;font-size:.875rem">
<style>
#quantity-demo .panel { display:grid; gap:.875rem; align-content:start; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
#quantity-demo .cap { margin:0; font-size:.75rem; font-weight:500; color:var(--mv-fg-muted) }
</style>
<form class="panel" id="quantity-demo-form" onsubmit="event.preventDefault()">
<p class="cap">In stock</p>
<mv-quantity label="Quantity" name="quantity" value="1" stock="24"></mv-quantity>
</form>
<div class="panel">
<p class="cap">Low stock · capped</p>
<mv-quantity label="Quantity" name="quantity-2" value="2" stock="3" data-variant="pill"></mv-quantity>
</div>
<div class="panel">
<p class="cap">Out of stock</p>
<mv-quantity label="Quantity" name="quantity-3" value="1" stock="0"></mv-quantity>
<button class="mv-button" data-variant="outline" data-size="sm" id="quantity-demo-restock" style="justify-self:start">Restock (+12)</button>
</div>
<div class="panel">
<p class="cap">Cart line (compact)</p>
<div style="display:flex;align-items:center;justify-content:space-between;gap:.75rem">
<span style="font-weight:500">Stoneware mug</span>
<mv-quantity value="2" max="10" data-size="sm" data-variant="ghost" caption="none" aria-label="Quantity, Stoneware mug"></mv-quantity>
</div>
<div style="display:flex;align-items:center;justify-content:space-between;gap:.75rem">
<span style="font-weight:500">Fig candle</span>
<mv-quantity value="1" max="10" data-size="sm" data-variant="ghost" caption="none" aria-label="Quantity, Fig candle"></mv-quantity>
</div>
</div>
</div>
<script type="module">
const root = document.getElementById("quantity-demo");
root.querySelector("#quantity-demo-restock").addEventListener("click", (e) => {
const q = root.querySelector('mv-quantity[name="quantity-3"]');
q.stock = (q.stock ?? 0) + 12;
e.currentTarget.disabled = true;
});
</script>
API
Attributes
Name Type Default Description value / name / labelnumber / string Same as <mv-number-field> (submits an integer value). minnumber 1Minimum quantity. maxnumber Commercial cap (e.g. 10 per order); combined with stock, the smaller one wins. stocknumber Available stock: caps the value and drives the caption; 0 = out of stock (disabled). low-stocknumber 5Threshold for the “Only n left in stock” caption. captionauto | none | text autoAutomatic caption, none, or free text. data-sizesm | md Height (lg by default, sized to match an “Add to cart” button). data-variantpill | ghost Rounded pill, or muted background without a border. disabled / readonly / requiredboolean Form states.
Properties
Name Type Description valuenumber | null Quantity. stocknumber Stock (assignable: restock). inStockboolean Read-only.
Methods
Name Description stepUp(n) / stepDown(n) / focus()Inherited from <mv-number-field>.
Events
Name Description mv-changedetail: { value, formatted } (inherited).
CSS classes
Name Description mv-quantity-captionStock caption; data-tone = success | warning | danger. data-stockOn the host: none | in | low | out.
CSS variables
Name Default Description --mv-quantity-width8.25remWidth.
Accessibility
Inherits the APG Spinbutton pattern from <mv-number-field> (arrows, Page↑/↓, Home/End, “Decrease” / “Increase” buttons outside the tab order). Default name “Quantity”. The stock caption is linked via aria-describedby; the “Maximum quantity” message is announced (aria-live polite) when trying to exceed the stock. When out of stock, the field is disabled and empty (dash).