Metric Tile <mv-metric-tile>

Tuile KPI autonome pour les tableaux de bord d’application, configurée uniquement par attributs : libellé avec icône facultative, un chiffre qui s’anime en comptant (number-ticker), son évolution par rapport à une valeur précédente (trend-chip), une sparkline d’historique (chart) ou une jauge vers un maximum (progress en anneau), une note et un pied, plus une taille compacte pour les grilles denses, comme huit tuiles de retards de paiement sur un même écran de finance.

CatégorieAffichage de données
TypeWeb Component (<mv-metric-tile>)
Statutstable
Installe aussichart, number-ticker, progress, trend-chip
Keywordskpi, metric, stat card, stat tile, dashboard, sparkline, gauge, trend, delta, finance, compact, app

When to use

  • An app dashboard needs a row of KPI tiles, each with its figure, its change vs the previous period and a small history line
  • A finance or ops screen packs many indicators, such as payment delays by bucket, into a dense grid of small tiles
  • A figure must be read against a goal, like quota attainment or budget used, with a ring gauge beside it
  • Tiles refresh with new values and the figures should roll to them while screen readers stay uninterrupted

Avoid when

  • A landing page shows key figures in editorial columns or marketing cards → use Stats instead
  • The trend itself is the subject and needs axes, tooltips and several series → use Chart instead
  • Only a change badge is needed inside a table or a sentence → use Trend Chip instead

Installation

node scripts/add.mjs metric-tile --out ./src/marvelous

Agent IA avec le serveur MCP de Marvelous UI : install_components({ slugs: ["metric-tile"], 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/motion.js, core/observe.js, components/chart/chart.js, components/chart/chart.css, components/number-ticker/number-ticker.js, components/number-ticker/number-ticker.css, components/progress/progress.css, components/trend-chip/trend-chip.js, components/trend-chip/trend-chip.css, components/metric-tile/metric-tile.js, components/metric-tile/metric-tile.css.

Utilisation

Balisage de référence : partez de celui-ci et personnalisez-le avec les attributs, data-* et les variables CSS :

<div id="mv-mt-demo" style="display:grid;gap:1.75rem;width:min(100%,68rem);margin-inline:auto">
  <div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,14.5rem),1fr));gap:1rem">
    <mv-metric-tile label="Revenue" value="48210" format="currency" previous="42900" period="vs last month"
      history="31200,34800,33100,39400,42800,41200,45900,42900,48210" history-labels="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug,Sep">
      <svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v18M16.5 7.5c0-1.7-2-3-4.5-3s-4.5 1.3-4.5 3 2 2.6 4.5 3 4.5 1.4 4.5 3.2-2 3-4.5 3-4.5-1.3-4.5-3"/></svg>
    </mv-metric-tile>

    <mv-metric-tile label="Days sales outstanding" value="42" unit="days" previous="47" change-format="day" better="down" period="vs last month"
      chart-type="line" history="51,49,50,47,45,47,42" history-labels="Mar,Apr,May,Jun,Jul,Aug,Sep" note="Target: 40 days or fewer">
      <svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><path d="M12 7.5V12l3 2"/></svg>
    </mv-metric-tile>

    <mv-metric-tile id="mv-mt-quota" label="Quota attainment" value="72400" format="currency" compact max="100000" previous="61800" period="vs last quarter"
      note="Q3 target: $100K, 9 days left">
      <svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8.5"/><circle cx="12" cy="12" r="4.5"/><circle cx="12" cy="12" r="0.5"/></svg>
    </mv-metric-tile>

    <mv-metric-tile label="Payments received" value="164" unit="invoices" previous="131" change-format="number" period="vs last week"
      chart-type="bar" history="96,118,104,142,121,131,164" history-labels="Week 33,Week 34,Week 35,Week 36,Week 37,Week 38,Week 39">
      <svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 3h9l4 4v14H6z"/><path d="M14 3v5h5M9.5 14l2 2 4-4"/></svg>
      <a slot="footer" href="#mv-mt-delays">Review late payments</a>
    </mv-metric-tile>
  </div>

  <section id="mv-mt-delays" aria-labelledby="mv-mt-delays-title" style="display:grid;gap:.85rem">
    <div style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem">
      <h3 id="mv-mt-delays-title" style="margin:0;font-size:var(--mv-text-base);font-weight:var(--mv-weight-semibold)">Payment delays <span style="color:var(--mv-fg-muted);font-weight:var(--mv-weight-medium)">September, compared with August</span></h3>
      <button class="mv-button" data-variant="outline" data-size="sm" id="mv-mt-next" type="button">Show October</button>
    </div>
    <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(min(100%,13rem),1fr));gap:.75rem">
      <mv-metric-tile data-size="compact" id="mv-mt-ontime" label="Paid on time" value="81.4" format="percent" decimals="1" previous="78.9" change-format="points" period="vs Aug"
        history="74.2,76.8,75.1,78.9,81.4" history-labels="May,Jun,Jul,Aug,Sep"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-delay" label="Average delay" value="6.8" unit="days" previous="8.1" change-format="day" better="down" period="vs Aug"
        chart-type="line" history="9.4,8.8,9.1,8.1,6.8" history-labels="May,Jun,Jul,Aug,Sep"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-15" label="1 to 15 days late" value="412300" format="currency" compact previous="455000" better="down" period="vs Aug"
        history="398000,431000,447000,455000,412300" history-labels="May,Jun,Jul,Aug,Sep"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-30" label="16 to 30 days late" value="186400" format="currency" compact previous="171200" better="down" period="vs Aug"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-60" label="31 to 60 days late" value="74100" format="currency" compact previous="92300" better="down" period="vs Aug"
        history="88000,95400,101200,92300,74100" history-labels="May,Jun,Jul,Aug,Sep"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-90" label="Over 60 days late" value="38200" format="currency" compact previous="36500" better="down" period="vs Aug"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-disputed" label="Disputed invoices" value="23" previous="23" change-format="number" better="down" period="vs Aug"
        chart-type="bar" history="31,27,29,23,23" history-labels="May,Jun,Jul,Aug,Sep"></mv-metric-tile>
      <mv-metric-tile data-size="compact" id="mv-mt-collected" label="Collected toward target" value="1860000" format="currency" compact max="2400000"></mv-metric-tile>
    </div>
  </section>
</div>
<script type="module">
  const root = document.getElementById("mv-mt-demo");
  const months = [
    { title: "September, compared with August", period: "vs Aug", data: {
      "mv-mt-ontime": [81.4, 78.9], "mv-mt-delay": [6.8, 8.1], "mv-mt-15": [412300, 455000], "mv-mt-30": [186400, 171200],
      "mv-mt-60": [74100, 92300], "mv-mt-90": [38200, 36500], "mv-mt-disputed": [23, 23], "mv-mt-collected": [1860000], "mv-mt-quota": [72400, 61800],
    } },
    { title: "October, compared with September", period: "vs Sep", data: {
      "mv-mt-ontime": [84.1, 81.4], "mv-mt-delay": [5.9, 6.8], "mv-mt-15": [389700, 412300], "mv-mt-30": [154900, 186400],
      "mv-mt-60": [79800, 74100], "mv-mt-90": [31600, 38200], "mv-mt-disputed": [19, 23], "mv-mt-collected": [2210000], "mv-mt-quota": [88900, 61800],
    } },
  ];
  let i = 0;
  const button = root.querySelector("#mv-mt-next");
  button.addEventListener("click", () => {
    i = (i + 1) % months.length;
    const month = months[i];
    for (const [id, [value, previous]] of Object.entries(month.data)) {
      const tile = root.querySelector(`#${id}`);
      if (previous !== undefined) tile.setAttribute("previous", previous);
      if (id !== "mv-mt-quota") tile.setAttribute("period", month.period);
      if (tile.hasAttribute("history")) {
        // October adds a point to the history (and drops the oldest one); September restores it.
        tile.sept ??= [tile.getAttribute("history"), tile.getAttribute("history-labels")];
        const [history, labels] = tile.sept;
        tile.setAttribute("history", i ? `${history.split(",").slice(1)},${value}` : history);
        tile.setAttribute("history-labels", i ? `${labels.split(",").slice(1)},Oct` : labels);
      }
      tile.setAttribute("value", value);
    }
    root.querySelector("#mv-mt-delays-title span").textContent = month.title;
    button.textContent = i === 0 ? "Show October" : "Show September";
  });
</script>

API

Attributes

NameTypeDefaultDescription
labelstringName of the metric. Shown above the figure and used as the tile's accessible name (role=group).
valuenumberThe figure. Changing it rolls the number to the new value and recomputes the change and the gauge. Without it the tile shows “-” and says “No data”.
unitstringText after the figure, smaller and muted (days, ms, invoices).
formatdecimal | currency | percent | compactdecimalIntl style of the figure, as in number-ticker. percent expects percentage points (42 = 42%).
currencyISO 4217 codeUSDCurrency for format=currency.
decimalsnumber(inferred from value)Fraction digits of the figure.
compactbooleanCompact notation, combinable with currency ($72.4K).
localeBCP 47nearest lang, else en-USFormatting locale for the figure, the gauge and the sparkline. The change follows the nearest lang, like trend-chip.
previousnumberValue of the previous period. With value, it drives the change chip (relative change by default).
changenumberA change computed elsewhere, used instead of previous (a fraction for percent and points: 0.124 = +12.4%).
change-formatpercent | points | number | currency | ISO 4217 code | Intl unitpercentHow the change is computed and printed (trend-chip's format). points with format=percent compares the two percentages in points; an Intl unit such as day or millisecond prints the difference in that unit.
betterup | down | neutralupWhich direction is good: colors and words of the change. Use down for delays, costs, churn.
periodstringWhat the change is measured against (vs last month).
historycomma-separated numbersPast values, oldest first, drawn as a sparkline (two values at least). An empty item is a gap.
history-labelscomma-separated stringsPoint 1, Point 2…Labels of the history values, read in the sparkline's data table.
chart-typearea | line | barareaSparkline style.
maxnumberShows a ring gauge of value between min and max, with the percentage in its center.
minnumber0Lower bound of the gauge.
notestringA muted line under the figures (target, context, last update).
animateview | noneviewview: the figure counts up and the sparkline draws in when the tile scrolls into view. none: everything shows at once.
data-sizeregular | compactregularcompact: smaller text and padding, the figure and the change share a line, and the sparkline or the gauge sits on the right, for dense grids of tiles.

Properties

NameTypeDefaultDescription
stringsobjectMvMetricTile.stringsTranslatable text merged over the defaults: empty (“-”), noValue (“No data”), gauge (“{percent} of {max}”, also {value}), chart (“{label}, history”) and point (“Point {n}”). Nested change and chart objects are handed to the trend chip's and the sparkline's own strings. Can be set before the element is defined.
ticker / chip / chartElement | nullRead-only: the inner mv-number-ticker, mv-trend-chip and mv-chart, for fine tuning (tile.chip.tolerance = 0.005). null before the first connection.
percentnumber | nullRead-only: where value stands between min and max (0 to 100), or null without a gauge.
label / value / previous / change / history / max …string | numberEvery attribute is mirrored as a property (camelCase: changeFormat, historyLabels, chartType); setting one updates the tile at once, even before the element is defined.

Methods

NameDescription
render()Syncs every part with the attributes now (rarely needed: changes already do it).

Events

NameDescription
mv-endBubbles from the inner number-ticker when the figure finishes counting (detail.value).

Content structure

NameDescription
iconA child with slot="icon" (an svg, an img, an icon element) moves to the left of the label. It is decorative: the tile hides it from screen readers.
footerChildren with slot="footer" (a link, a button, a timestamp) go to a footer row under a hairline.

CSS classes

NameDescription
mv-metric-tile-body / -mainGenerated: the figures column and, beside it, the gauge.
mv-metric-tile-head / -icon / -labelGenerated: the label row.
mv-metric-tile-figure / -unit / -emptyGenerated: the figure (an mv-number-ticker), its unit, and the “-” shown without a value.
mv-metric-tile-changeGenerated: the mv-trend-chip.
mv-metric-tile-gaugeGenerated: the ring gauge (a .mv-progress-ring with role=meter).
mv-metric-tile-chartGenerated: the mv-chart sparkline.
mv-metric-tile-note / -footerGenerated: the note line and the footer row.

CSS variables

NameDefaultDescription
--mv-metric-tile-bgvar(--mv-surface)Tile background.
--mv-metric-tile-bordervar(--mv-border)Tile border color.
--mv-metric-tile-radiusvar(--mv-radius-xl) (lg in compact)Corner radius.
--mv-metric-tile-shadowvar(--mv-shadow-xs)Tile shadow.
--mv-metric-tile-paddingvar(--mv-space-5) (space-3 in compact)Inner padding; the sparkline bleeds over it.
--mv-metric-tile-value-sizevar(--mv-text-3xl) (text-xl in compact)Font size of the figure.
--mv-metric-tile-icon-bg / -icon-color / -icon-boxvar(--mv-bg-muted) / var(--mv-fg) / 1.75remIcon square (compact shows the bare icon).
--mv-metric-tile-chart-colorvar(--mv-accent)Sparkline color. Tip: mv-metric-tile[data-tone="bad"] { --mv-metric-tile-chart-color: var(--mv-danger) }.
--mv-metric-tile-chart-width4.5remSparkline width in the compact size.
--mv-metric-tile-gauge-colorvar(--mv-accent)Gauge fill color.

Accessibility

The tile is a role=group named by its label, so a screen reader announces “Revenue, group” and then reads, in order: the figure (number-ticker's hidden final value: the counting digits are aria-hidden and never read in bursts) with its unit, the change as one whole sentence from trend-chip (“Up 12.4% vs last month, an improvement”, “Down 5 days vs last month, an improvement” with better="down"), the gauge as role=meter named by the label with a spoken value (“72% of $100K”), the sparkline as chart's labelled group with its visually hidden data table, then the note and the footer. The icon is aria-hidden. Without a value the figure shows “-” and says “No data”. Updates are silent (no live region): a refreshing dashboard never interrupts the reader. Nothing is focusable except what you put in the footer; the compact sparkline is not interactive either. Meaning never depends on color: the change carries a glyph and words, and data-tone / data-dir (good, bad, neutral; up, down, flat) are exposed on the tile for styling only. Reduced motion: the figure, the gauge and the sparkline show their final state at once. Forced colors: the tile keeps a CanvasText frame, the chip and the chart switch to their own system-color styles. Known limits: the gauge animates on first display, not on scroll into view; the change chip reads its locale from the nearest lang rather than the locale attribute; tolerance and digits of the change are set on tile.chip.