Price <mv-price>
Prix e-commerce formaté avec Intl.NumberFormat (en-US / USD par défaut) : prix actuel, prix de comparaison barré, badge de remise calculé (pourcentage ou montant), prix unitaire, « À partir de », fourchette de prix, centimes en exposant, quatre tailles et un équivalent dans une devise de référence affiché au survol et au focus (taux fourni par l’application, jamais récupéré) ; utilisable aussi en HTML statique avec les classes .mv-price-*.
| Catégorie | Affichage de données |
|---|---|
| Type | Web Component (<mv-price>) |
| Statut | stable |
| Keywords | price, prix, sale, soldes, compare-at, discount, unit-price, currency, intl, e-commerce, exchange-rate, currency-conversion, reference-currency |
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
- A store charges in one currency and visitors want an indicative equivalent in their own, at a rate the app provides
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
Installation
node scripts/add.mjs price --out ./src/marvelousAgent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["price"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).
Fichiers copiés (dépendances comprises) : tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/i18n.js, core/motion.js, core/position.js, components/price/price.js, components/price/price.css.
Utilisation
Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :
<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">Reference currency · tooltip</p>
<p class="name">Leather weekender bag</p>
<mv-price value="249" compare-at="299" reference-currency="EUR" rate="0.8531" rate-note="Indicative rate, Oct 3" data-size="lg"></mv-price>
</div>
<div class="tile">
<p class="cap">Reference currency · inline</p>
<p class="name">Linen throw blanket</p>
<mv-price value="64" currency="GBP" locale="en-GB" reference-currency="USD" rate="1.3412" reference-display="inline"></mv-price>
</div>
<div class="tile">
<p class="cap">Range · equivalent in yen</p>
<p class="name">Ceramic pour-over set · 3 sizes</p>
<mv-price from value="38" max="72" reference-currency="JPY" rate="147.32"></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 | Par défaut | 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 (BCP 47). The value auto follows the page language (nearest lang); without the attribute, en-US unless the app registered the page language with setStrings() (core/i18n.js). |
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. | |
reference-currency | string | ISO 4217 code of the equivalent (“≈ €82.40”). Shown only with a valid rate and a currency different from currency. | |
rate | number | Units of reference-currency for 1 unit of currency (USD to EUR: 0.9258). Supplied by the application; the component never fetches a rate. Applies to value and max. | |
rate-note | string | Extra line in the tooltip, such as the source or date of the rate (“Indicative rate, Oct 3”). | |
reference-display | tooltip | inline | tooltip | tooltip: the price gets a dotted underline and shows the equivalent and the rate on hover and keyboard focus. inline: muted “≈ €82.40” text after the price, no tab stop (use it inside a link or a button). |
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). |
equivalent | string | Formatted equivalent in the reference currency, a range when max is set; empty without a usable rate (read-only). |
strings | Partial<Record<string, string>> | Overrides for the default text: from, salePrice, originalPrice, discount, approx (“≈ {amount}”, visible), approxLabel (“(about {amount})”, read by screen readers), rate (“1 {from} = {rate} {to}”). English defaults. Can be set before the element is defined. |
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. |
formatEquivalent(value, { rate, currency, locale }) / convertMoney(value, rate) / isCurrency(code) | Exports: equivalent rounded to the digits of the target currency (none for JPY), empty string when the rate or code is not usable; raw conversion (NaN if unusable); ISO 4217 check. |
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). |
mv-price-equivalent | Inline equivalent (reference-display="inline"). |
mv-price-tip / -tip-amount / -tip-rate / -tip-note | Tooltip bubble (top layer, Popover API) and its lines. |
CSS variables
| Name | Par défaut | Description |
|---|---|---|
--mv-price-sale-color | softened danger | Sale price color. |
--mv-price-tip-bg / --mv-price-tip-fg | inverted gray | Tooltip colors. |
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. Reference currency: the equivalent is always in the accessibility tree as hidden text inside the price (“(about €82.40)”), so it is read in browse mode without hovering; in tooltip mode the price becomes a tab stop with a dotted underline, the tooltip (role=tooltip, top layer) opens on hover after 200 ms and immediately when the price receives focus (keyboard, or a tap in browsers that focus it), stays open while the pointer moves onto it, closes with Escape, and its rate line is the price's aria-describedby. Its visible amount is aria-hidden to avoid a double reading. Inside a link or a button, use reference-display="inline" to avoid a nested tab stop. Value change (variant): subtle fade, removed with reduced motion.