エクスクルーシブ
Trend Chip <mv-trend-chip>
KPI カード、ダッシュボード、料金表、レポートのための小さな「▲ +12.4% vs last week」チップ:変化量(value="0.124")または 2 つの値(from / to)を渡すと、符号付きの値を Intl でパーセント、パーセントポイント、通常の数値、任意の通貨、任意の単位(「−32 ms」)として表示し、期間も任意で添えられます。
仕組み
KPI カード、ダッシュボード、料金表、レポートのための小さな「▲ +12.4% vs last week」チップ:変化量(value="0.124")または 2 つの値(from / to)を渡すと、符号付きの値を Intl でパーセント、パーセントポイント、通常の数値、任意の通貨、任意の単位(「−32 ms」)として表示し、期間も任意で添えられます。better="down" を指定すると、解約率、レイテンシ、コストの低下が良い結果として表示されます。トーンは色だけでなく形でも伝わり(良い場合は淡い塗りの上の塗りつぶした三角、悪い場合は枠線付きチップの中の中抜きの三角、中立は細い矢印、横ばいはダッシュ)、許容幅を設定すると小さな変動をほぼ横ばいとして扱えます。他の実装が見落とす点:スクリーンリーダーには「plus 12 percent up arrow」ではなく、ひとつの正しい文(「Up 12.4% vs last week, an improvement」「Down 0.3 percentage points vs last month, an improvement」)が読み上げられます。符号と丸めは正確で(本物のマイナス記号を使い、「−0.0%」は決して表示せず、精度に対して小さすぎる変化は誤解を招くゼロではなく「<0.1%」と表示します)、JS なしでも読めます:内部のサーバー側テキストはスクリプトの実行まで保持され、value 属性だけで CSS によりトーンと矢印が決まり、純粋関数としてエクスポートされる describe() がサーバーレンダリング用に同じテキスト、文、トーンを返します。
| カテゴリー | データ表示 |
|---|---|
| タイプ | Web Component(<mv-trend-chip>) |
| ステータス | 安定版 |
| Keywords | exclusive, light, trend, delta, change, kpi, dashboard, percent, badge, intl, ssr, a11y |
When to use
- A KPI card or dashboard tile shows how a figure moved since the previous period
- A report or pricing table lists changes where some metrics are better when they go down, like churn, cost or latency
- A change badge must not rely on color alone and must be read as a sentence by screen readers
- Server-rendered pages should print the change in HTML and keep it readable before or without JavaScript
Avoid when
- The change must be judged against inflation, a benchmark or peers rather than against zero → use Real Terms instead
- A whole section of headline figures with counters and sparklines is needed → use Stats instead
- The number itself should count up or roll when it changes → use Number Ticker instead
インストール
node scripts/add.mjs trend-chip --out ./src/marvelousMarvelous UI の MCP サーバーを使う AI エージェント:install_components({ slugs: ["trend-chip"], target_dir: "<absolute path>/src/marvelous", framework: "react" })。
コピーされるファイル(依存関係を含む):tokens/tokens.css, core/base.css, core/element.js, components/trend-chip/trend-chip.js, components/trend-chip/trend-chip.css。
使い方
クイックスタート、動作する最小限のマークアップ:
<mv-trend-chip value="0.124" period="vs last week"></mv-trend-chip>基本となるマークアップです。ここから属性、data-*、CSS 変数でカスタマイズしてください:
<div id="mv-tc-demo" style="display:grid;gap:1.25rem;width:min(100%,52rem);margin-inline:auto">
<div style="display:grid;grid-template-columns:repeat(auto-fit,minmax(11.5rem,1fr));gap:.75rem">
<section style="display:grid;gap:.45rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);box-shadow:var(--mv-shadow-xs)">
<h3 style="margin:0;font-size:.8rem;font-weight:500;color:var(--mv-fg-muted)">Revenue</h3>
<p style="margin:0;font-size:1.5rem;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums">$48,210</p>
<mv-trend-chip id="mv-tc-revenue" value="0.124" period="vs last week">+12.4% vs last week</mv-trend-chip>
</section>
<section style="display:grid;gap:.45rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);box-shadow:var(--mv-shadow-xs)">
<h3 style="margin:0;font-size:.8rem;font-weight:500;color:var(--mv-fg-muted)">Churn rate</h3>
<p style="margin:0;font-size:1.5rem;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums">2.1%</p>
<mv-trend-chip from="0.024" to="0.021" format="points" better="down" period="vs last month"></mv-trend-chip>
</section>
<section style="display:grid;gap:.45rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);box-shadow:var(--mv-shadow-xs)">
<h3 style="margin:0;font-size:.8rem;font-weight:500;color:var(--mv-fg-muted)">p95 latency</h3>
<p style="margin:0;font-size:1.5rem;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums">214 ms</p>
<mv-trend-chip from="182" to="214" format="millisecond" better="down" period="vs last week"></mv-trend-chip>
</section>
<section style="display:grid;gap:.45rem;padding:1rem;border:1px solid var(--mv-border);border-radius:var(--mv-radius-lg);background:var(--mv-surface);box-shadow:var(--mv-shadow-xs)">
<h3 style="margin:0;font-size:.8rem;font-weight:500;color:var(--mv-fg-muted)">Active users</h3>
<p style="margin:0;font-size:1.5rem;font-weight:600;letter-spacing:-.02em;font-variant-numeric:tabular-nums">12,480</p>
<mv-trend-chip value="-0.036" period="vs last week"></mv-trend-chip>
</section>
</div>
<div style="display:flex;flex-wrap:wrap;align-items:center;gap:.75rem;font-size:.8125rem;color:var(--mv-fg-muted)">
<button class="mv-button" data-variant="outline" data-size="sm" id="mv-tc-refresh">Load next week</button>
<span>Screen readers hear: <q id="mv-tc-said" style="color:var(--mv-fg)"></q></span>
</div>
<div class="mv-table-wrap">
<table class="mv-table">
<caption>September report. Plain variant; costs and refunds are lower-is-better.</caption>
<thead><tr><th>Metric</th><th data-align="end">September</th><th data-align="end">Change vs August</th></tr></thead>
<tbody>
<tr><td>Support cost</td><td data-align="end">$17,950</td><td data-align="end"><mv-trend-chip data-variant="plain" id="mv-tc-cost" from="18400" to="17950" format="currency" better="down"></mv-trend-chip></td></tr>
<tr><td>Sign-ups</td><td data-align="end">1,204,410</td><td data-align="end"><mv-trend-chip data-variant="plain" from="1204300" to="1204410"></mv-trend-chip></td></tr>
<tr><td>Refund rate (±0.5% counts as flat)</td><td data-align="end">1.9%</td><td data-align="end"><mv-trend-chip data-variant="plain" id="mv-tc-refunds" value="0.003" better="down"></mv-trend-chip></td></tr>
<tr><td>Uptime</td><td data-align="end">99.95%</td><td data-align="end"><mv-trend-chip data-variant="plain" from="0.9995" to="0.9995" format="points"></mv-trend-chip></td></tr>
<tr><td>Headcount</td><td data-align="end">45</td><td data-align="end"><mv-trend-chip data-variant="plain" from="42" to="45" format="number" better="neutral"></mv-trend-chip></td></tr>
</tbody>
</table>
</div>
</div>
<script type="module">
const root = document.getElementById("mv-tc-demo");
const revenue = root.querySelector("#mv-tc-revenue");
const said = root.querySelector("#mv-tc-said");
root.querySelector("#mv-tc-cost").digits = 0;
root.querySelector("#mv-tc-refunds").tolerance = 0.005;
const weeks = ["0.124", "-0.048", "0.0004", "0"];
let i = 0;
const show = () => customElements.whenDefined("mv-trend-chip").then(() => { said.textContent = revenue.state.label; });
root.querySelector("#mv-tc-refresh").addEventListener("click", () => {
i = (i + 1) % weeks.length;
revenue.value = weeks[i];
show();
});
show();
</script>API
Attributes
| Name | タイプ | Default | Description |
|---|---|---|---|
value | number | The change. For percent and points it is a fraction (0.124 = +12.4%, 0.003 = +0.3 pts); for number, currency and units it is the difference itself. Takes precedence over from / to. | |
from / to | number | Or give the previous and current amounts: percent computes (to − from) / |from| (from = 0 gives no comparison), the other formats use to − from. For points, give rates as fractions (0.024 → 0.021). | |
format | percent | points | number | currency | ISO 4217 code | Intl unit | percent | How the change is printed, always through Intl: “+12.4%”, “−0.3 pts”, “+140”, “−$450.00” (currency = USD, or a code such as EUR), or a unit such as millisecond, kilobyte or hour (“−32 ms”). |
period | string | What the change is measured against, shown after the chip and placed in the sentence: “vs last week”. | |
better | up | down | neutral | up | Which direction is an improvement. down: a fall reads as good (churn, cost, latency). neutral: no judgment, only the direction. |
lang (inherited) | BCP 47 | Number locale from the nearest [lang] ancestor, else en-US. | |
data-variant | plain | Styling hook: colored text and glyph without the pill, for dense tables and inline copy (bad changes get a dotted underline as a second cue). | |
data-dir / data-tone | up | down | flat | none / good | bad | neutral | Set by the component (for styling). A server can write them too, from describe(). |
Properties
| Name | タイプ | Default | Description |
|---|---|---|---|
tolerance | number | 0 | A change whose size is at or below this (same units as value: 0.005 = half a percent) is about flat: dash glyph, neutral tone, and a sentence that still says which way it moved (“About flat vs last month, up 0.3%”). |
digits | number | 1 for percent and points, the currency's own for money, up to 1 otherwise | Fraction digits. Percent and points always print exactly this many, so every chip on a page rounds the same way. |
strings | object | MvTrendChip.strings | Translatable text, merged over the defaults: whole sentences upGood, upBad, up, downGood, downBad, down, flat, nearUp, nearDown, none with {value} and {period} placeholders, plus under (“<{value}”), underLong (“less than {value}”), points, pointsLong and empty (“-”). |
state | { dir, tone, text, period, label, change } | Read-only: the current result, including the spoken sentence (label) and the raw change. | |
value / from / to / format / period / better | string | Mirror the attributes; changes re-render at once. All can be set before the element is defined. |
Methods
| Name | Description |
|---|---|
describe({ value, from, to, format, period, better, tolerance, digits, locale, strings }) | Named export (and static MvTrendChip.describe): pure and server-safe, returns { dir, tone, text, period, label, change }, the exact visible text and sentence the element renders. |
render() | Re-render now (rarely needed: attribute and property changes already do it). |
Content structure
| Name | Description |
|---|---|
(text) | Server-rendered fallback, read as is before the script loads; replaced by the parts once it runs. Without value or from / to, the text is left untouched. |
.mv-trend-chip-value / -period / -label | Or render the final parts on the server (from describe()): they are adopted in place, so nothing shifts on upgrade. |
CSS classes
| Name | Description |
|---|---|
mv-trend-chip-value | The pill: glyph (::before, a CSS mask) and signed value; aria-hidden. |
mv-trend-chip-period | The muted period text; aria-hidden (it is part of the sentence). |
mv-trend-chip-label | The visually hidden sentence read by screen readers (mv-sr-only). |
CSS variables
| Name | Default | Description |
|---|---|---|
--mv-trend-chip-good | var(--mv-success) | Good tone. |
--mv-trend-chip-bad | var(--mv-danger) | Bad tone. |
--mv-trend-chip-neutral | var(--mv-fg-muted) | Neutral and flat tone. |
--mv-trend-chip-font-size | var(--mv-text-xs) | Text size (the glyph scales with it). |
--mv-trend-chip-radius | var(--mv-radius-full) | Pill corner radius. |
--mv-trend-chip-gap | 0.4em | Space between the pill and the period. |
Accessibility
Plain inline content, no role and no tab stop. The visible pill and period are aria-hidden and a visually hidden span carries one complete sentence built from a whole-sentence template (never glued fragments): “Up 12.4% vs last week, an improvement”, “Down 32 ms vs last week, an improvement” with better="down", “Up 3” with better="neutral", “Unchanged vs yesterday”, “About flat vs last month, up 0.3%” inside the tolerance, “Up less than 0.1% vs yesterday, an improvement” for tiny changes, and “No comparison available” when the change cannot be computed. The number in the sentence is unsigned (the word carries the direction), so nothing reads “plus” or “minus” or “up arrow”. Updates are silent (no live region): a dashboard refreshing its tiles never interrupts the reader, and the text is only written when it actually changes. Meaning never depends on color alone (WCAG 1.4.1): direction is the glyph's orientation, good is a filled triangle, bad is a hollow triangle in an outlined chip (plus a dotted underline in the plain variant), neutral is a thin arrow and flat is a dash. Text colors are mixed toward the foreground for contrast on the tinted pill, in light and dark themes. Forced colors: the glyph is drawn in CanvasText, the chip gets a border, dashed when the change is bad. The glyph's turn when the direction flips uses the motion tokens and is removed under prefers-reduced-motion and [data-motion="reduce"]. Before or without JS, server-rendered text inside the element is read as written, and CSS already derives tone and direction from the value attribute. Known limits: the locale is read from the nearest lang when the chip renders, so a later lang switch shows once an attribute or property changes (or call render()); tolerance, digits and strings are properties only, with no attribute; unit formats accept only the units Intl.NumberFormat knows (millisecond, kilobyte, celsius…), others fall back to a plain number.