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.

CategoryForms
TypeWeb Component (<mv-quantity>)
Statusstable
Also installsnumber-field
Keywordsquantity, quantité, stepper, stock, out-of-stock, cart, e-commerce, form-associated

When to use

Avoid when

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

NameTypeDefaultDescription
value / name / labelnumber / stringSame as <mv-number-field> (submits an integer value).
minnumber1Minimum quantity.
maxnumberCommercial cap (e.g. 10 per order); combined with stock, the smaller one wins.
stocknumberAvailable stock: caps the value and drives the caption; 0 = out of stock (disabled).
low-stocknumber5Threshold for the “Only n left in stock” caption.
captionauto | none | textautoAutomatic caption, none, or free text.
data-sizesm | mdHeight (lg by default, sized to match an “Add to cart” button).
data-variantpill | ghostRounded pill, or muted background without a border.
disabled / readonly / requiredbooleanForm states.

Properties

NameTypeDescription
valuenumber | nullQuantity.
stocknumberStock (assignable: restock).
inStockbooleanRead-only.

Methods

NameDescription
stepUp(n) / stepDown(n) / focus()Inherited from <mv-number-field>.

Events

NameDescription
mv-changedetail: { value, formatted } (inherited).

CSS classes

NameDescription
mv-quantity-captionStock caption; data-tone = success | warning | danger.
data-stockOn the host: none | in | low | out.

CSS variables

NameDefaultDescription
--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).