Price — <mv-price>

E-commerce price formatted with Intl.NumberFormat (en-US / USD by default): current price, strikethrough compare-at price, computed discount badge (percent or amount), unit price, “From”, price range, superscript cents, four sizes; also usable as static HTML with the .mv-price-* classes.

CategoryData display
TypeWeb Component (<mv-price>)
Statusstable
Keywordsprice, prix, sale, soldes, compare-at, discount, unit-price, currency, intl, e-commerce

When to use

Avoid when

Install

node scripts/add.mjs price --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["price"], 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/price/price.js, components/price/price.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="price-demo" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,15rem),1fr));gap:1.25rem;width:min(100%,52rem);margin-inline:auto">
  <style>
    #price-demo .tile { display:grid; gap:.625rem; align-content:start; padding:1.25rem; border:1px solid var(--mv-border); border-radius:var(--mv-radius-xl); background:var(--mv-surface) }
    #price-demo .cap { margin:0; font-size:.75rem; font-weight:500; color:var(--mv-fg-muted); letter-spacing:.02em }
    #price-demo .name { margin:0; font-size:.875rem; font-weight:500 }
  </style>

  <div class="tile" style="grid-column:1/-1">
    <p class="cap">Product page · sale</p>
    <p class="name" style="font-size:1.125rem">Merino crewneck sweater</p>
    <mv-price id="price-demo-pdp" value="89" compare-at="119" note="+ tax" data-size="xl"></mv-price>
    <div style="display:flex;gap:.5rem;flex-wrap:wrap">
      <button class="mv-button" data-variant="outline" data-size="sm" data-price="89" aria-pressed="true">Sand · $89</button>
      <button class="mv-button" data-variant="outline" data-size="sm" data-price="99">Midnight · $99</button>
      <button class="mv-button" data-variant="outline" data-size="sm" data-price="119">Limited edition · $119</button>
    </div>
  </div>

  <div class="tile">
    <p class="cap">Grid thumbnail</p>
    <p class="name">Glazed stoneware mug</p>
    <mv-price value="24.9" compare-at="32" data-size="sm"></mv-price>
  </div>

  <div class="tile">
    <p class="cap">Price per ounce</p>
    <p class="name">Insulated bottle, 25 oz</p>
    <mv-price value="34.5" unit-price="1.38" unit="oz"></mv-price>
  </div>

  <div class="tile">
    <p class="cap">Price range</p>
    <p class="name">Scented candle · 3 sizes</p>
    <mv-price from value="18" max="46"></mv-price>
  </div>

  <div class="tile">
    <p class="cap">Superscript cents</p>
    <p class="name">Wave wireless headphones</p>
    <mv-price value="189.9" compare-at="229.9" data-variant="superscript" data-size="lg" discount="amount"></mv-price>
  </div>

  <div class="tile">
    <p class="cap">Other currency · de-DE</p>
    <p class="name">Canvas tote bag</p>
    <mv-price value="42" compare-at="56" currency="EUR" locale="de-DE"></mv-price>
  </div>

  <div class="tile">
    <p class="cap">Static HTML (no JS)</p>
    <p class="name">Opaline lamp</p>
    <span class="mv-price" data-sale>
      <span class="mv-price-current"><span class="mv-sr-only">Sale price: </span>$129</span>
      <s class="mv-price-compare"><span class="mv-sr-only">Original price: </span>$159</s>
      <span class="mv-price-discount">−19%</span>
    </span>
  </div>
</div>
<script type="module">
  const root = document.getElementById("price-demo");
  const pdp = root.querySelector("#price-demo-pdp");
  root.addEventListener("click", (e) => {
    const btn = e.target.closest("[data-price]");
    if (!btn) return;
    root.querySelectorAll("[data-price]").forEach((b) => b.setAttribute("aria-pressed", String(b === btn)));
    pdp.value = Number(btn.dataset.price);
  });
</script>

API

Attributes

NameTypeDefaultDescription
valuenumberCurrent price (currency units, e.g. 24.9).
compare-atnumberOriginal price; greater than value = on sale (sale color, strikethrough price, badge).
maxnumberUpper bound of a range (“$18 – $46”).
currencystringUSDISO 4217 code.
localestringen-USFormatting locale.
decimalsauto | 0 | 2autoauto: “$89” but “$24.90”.
discountpercent | amount | nonepercentBadge “−25%”, “−$30” or none.
unit-price / unitnumber / stringUnit price: “$1.38 / oz”.
fromboolean“From” prefix.
notestringNote (“+ tax”, “incl. VAT”, “/ month”).
labelstringPrefix read by screen readers when the price is not on sale.
data-sizesm | lg | xlSize (md by default).
data-variantsuperscriptSuperscript cents ($189⁹⁰).
data-layoutstacked | inlineStacked, or unit price on the same line.
data-toneneutralKeeps the normal color for a sale price.

Properties

NameTypeDescription
formattedstringFormatted current price (read-only).

Methods

NameDescription
formatMoney(value, { currency, locale, decimals })Module export, reused by the other e-commerce components.
moneyFormatter(options) / formatPercent(value, locale)Exports: cached Intl formatter, signed percentage.

CSS classes

NameDescription
mv-priceContainer; data-sale when on sale (set automatically by <mv-price>).
mv-price-current / -compare / -discount / -unit / -from / -note / -fractionParts (generated or hand-written).

CSS variables

NameDefaultDescription
--mv-price-sale-colorsoftened dangerSale price color.

Accessibility

The strikethrough price is an <s> prefixed with hidden “Original price:” text, the current price is prefixed with “Sale price:” when discounted, and the badge with “Discount:”. Screen readers never have to interpret a visual strikethrough. Value change (variant): subtle fade, removed with reduced motion.