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égorieAffichage de données
TypeWeb Component (<mv-price>)
Statutstable
Keywordsprice, 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/marvelous

Agent 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

NameTypePar défautDescription
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 (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).
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.
reference-currencystringISO 4217 code of the equivalent (“≈ €82.40”). Shown only with a valid rate and a currency different from currency.
ratenumberUnits 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-notestringExtra line in the tooltip, such as the source or date of the rate (“Indicative rate, Oct 3”).
reference-displaytooltip | inlinetooltiptooltip: 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-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).
equivalentstringFormatted equivalent in the reference currency, a range when max is set; empty without a usable rate (read-only).
stringsPartial<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

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

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).
mv-price-equivalentInline equivalent (reference-display="inline").
mv-price-tip / -tip-amount / -tip-rate / -tip-noteTooltip bubble (top layer, Popover API) and its lines.

CSS variables

NamePar défautDescription
--mv-price-sale-colorsoftened dangerSale price color.
--mv-price-tip-bg / --mv-price-tip-fginverted grayTooltip 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.