Metric Tile <mv-metric-tile>
用于应用仪表盘的独立 KPI 卡片,仅通过属性即可配置:带可选图标的标签,一个向上滚动计数的数字(number-ticker),它相对前值的变化(trend-chip),历史迷你折线图(chart)或指向最大值的仪表(progress ring),一条备注和一个页脚,另有适合密集网格的紧凑尺寸,例如在一个财务页面上放八个付款延迟卡片。
| 分类 | 数据展示 |
|---|---|
| 类型 | Web Component(<mv-metric-tile>) |
| 状态 | 稳定版 |
| 同时安装 | chart, number-ticker, progress, trend-chip |
| Keywords | kpi, 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
安装
node scripts/add.mjs metric-tile --out ./src/marvelous使用 Marvelous UI MCP 服务器的 AI 智能体:install_components({ slugs: ["metric-tile"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
复制的文件(含依赖):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。
用法
标准标记,可在此基础上通过属性、data-* 和 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
| Name | 类型 | Default | Description |
|---|---|---|---|
label | string | Name of the metric. Shown above the figure and used as the tile's accessible name (role=group). | |
value | number | The 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”. | |
unit | string | Text after the figure, smaller and muted (days, ms, invoices). | |
format | decimal | currency | percent | compact | decimal | Intl style of the figure, as in number-ticker. percent expects percentage points (42 = 42%). |
currency | ISO 4217 code | USD | Currency for format=currency. |
decimals | number | (inferred from value) | Fraction digits of the figure. |
compact | boolean | Compact notation, combinable with currency ($72.4K). | |
locale | BCP 47 | nearest lang, else en-US | Formatting locale for the figure, the gauge and the sparkline. The change follows the nearest lang, like trend-chip. |
previous | number | Value of the previous period. With value, it drives the change chip (relative change by default). | |
change | number | A change computed elsewhere, used instead of previous (a fraction for percent and points: 0.124 = +12.4%). | |
change-format | percent | points | number | currency | ISO 4217 code | Intl unit | percent | How 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. |
better | up | down | neutral | up | Which direction is good: colors and words of the change. Use down for delays, costs, churn. |
period | string | What the change is measured against (vs last month). | |
history | comma-separated numbers | Past values, oldest first, drawn as a sparkline (two values at least). An empty item is a gap. | |
history-labels | comma-separated strings | Point 1, Point 2… | Labels of the history values, read in the sparkline's data table. |
chart-type | area | line | bar | area | Sparkline style. |
max | number | Shows a ring gauge of value between min and max, with the percentage in its center. | |
min | number | 0 | Lower bound of the gauge. |
note | string | A muted line under the figures (target, context, last update). | |
animate | view | none | view | view: the figure counts up and the sparkline draws in when the tile scrolls into view. none: everything shows at once. |
data-size | regular | compact | regular | compact: 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
| Name | 类型 | Default | Description |
|---|---|---|---|
strings | object | MvMetricTile.strings | Translatable 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 / chart | Element | null | Read-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. | |
percent | number | null | Read-only: where value stands between min and max (0 to 100), or null without a gauge. | |
label / value / previous / change / history / max … | string | number | Every attribute is mirrored as a property (camelCase: changeFormat, historyLabels, chartType); setting one updates the tile at once, even before the element is defined. |
Methods
| Name | Description |
|---|---|
render() | Syncs every part with the attributes now (rarely needed: changes already do it). |
Events
| Name | Description |
|---|---|
mv-end | Bubbles from the inner number-ticker when the figure finishes counting (detail.value). |
Content structure
| Name | Description |
|---|---|
icon | A 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. |
footer | Children with slot="footer" (a link, a button, a timestamp) go to a footer row under a hairline. |
CSS classes
| Name | Description |
|---|---|
mv-metric-tile-body / -main | Generated: the figures column and, beside it, the gauge. |
mv-metric-tile-head / -icon / -label | Generated: the label row. |
mv-metric-tile-figure / -unit / -empty | Generated: the figure (an mv-number-ticker), its unit, and the “-” shown without a value. |
mv-metric-tile-change | Generated: the mv-trend-chip. |
mv-metric-tile-gauge | Generated: the ring gauge (a .mv-progress-ring with role=meter). |
mv-metric-tile-chart | Generated: the mv-chart sparkline. |
mv-metric-tile-note / -footer | Generated: the note line and the footer row. |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-metric-tile-bg | var(--mv-surface) | Tile background. |
--mv-metric-tile-border | var(--mv-border) | Tile border color. |
--mv-metric-tile-radius | var(--mv-radius-xl) (lg in compact) | Corner radius. |
--mv-metric-tile-shadow | var(--mv-shadow-xs) | Tile shadow. |
--mv-metric-tile-padding | var(--mv-space-5) (space-3 in compact) | Inner padding; the sparkline bleeds over it. |
--mv-metric-tile-value-size | var(--mv-text-3xl) (text-xl in compact) | Font size of the figure. |
--mv-metric-tile-icon-bg / -icon-color / -icon-box | var(--mv-bg-muted) / var(--mv-fg) / 1.75rem | Icon square (compact shows the bare icon). |
--mv-metric-tile-chart-color | var(--mv-accent) | Sparkline color. Tip: mv-metric-tile[data-tone="bad"] { --mv-metric-tile-chart-color: var(--mv-danger) }. |
--mv-metric-tile-chart-width | 4.5rem | Sparkline width in the compact size. |
--mv-metric-tile-gauge-color | var(--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.