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.
| Category | Data display |
|---|---|
| Type | Web Component (<mv-price>) |
| Status | stable |
| Keywords | price, prix, sale, soldes, compare-at, discount, unit-price, currency, intl, e-commerce |
When to use
- A product price must be formatted for a locale and currency, with a struck compare-at price and discount badge
- A listing shows a price range, a From prefix or a unit price such as per ounce
- A subscription or quote shows a price with a note like per month or plus tax
Avoid when
- Several subscription plans must be compared with features and a billing switch → use Pricing instead
- The figure is a KPI or stat that should animate between values → use Number Ticker instead
Install
node scripts/add.mjs price --out ./src/marvelousAI 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
| Name | Type | Default | Description |
|---|---|---|---|
value | number | Current price (currency units, e.g. 24.9). | |
compare-at | number | Original price; greater than value = on sale (sale color, strikethrough price, badge). | |
max | number | Upper bound of a range (“$18 – $46”). | |
currency | string | USD | ISO 4217 code. |
locale | string | en-US | Formatting locale. |
decimals | auto | 0 | 2 | auto | auto: “$89” but “$24.90”. |
discount | percent | amount | none | percent | Badge “−25%”, “−$30” or none. |
unit-price / unit | number / string | Unit price: “$1.38 / oz”. | |
from | boolean | “From” prefix. | |
note | string | Note (“+ tax”, “incl. VAT”, “/ month”). | |
label | string | Prefix read by screen readers when the price is not on sale. | |
data-size | sm | lg | xl | Size (md by default). | |
data-variant | superscript | Superscript cents ($189⁹⁰). | |
data-layout | stacked | inline | Stacked, or unit price on the same line. | |
data-tone | neutral | Keeps the normal color for a sale price. |
Properties
| Name | Type | Description |
|---|---|---|
formatted | string | Formatted current price (read-only). |
Methods
| Name | Description |
|---|---|
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
| Name | Description |
|---|---|
mv-price | Container; data-sale when on sale (set automatically by <mv-price>). |
mv-price-current / -compare / -discount / -unit / -from / -note / -fraction | Parts (generated or hand-written). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-price-sale-color | softened danger | Sale 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.