Exclusivebeta

Yet To Come — <mv-yet-to-come>

A metric told as a three-part story, built to change what happens next: Past (sparkline, change over the period and the biggest jump or drop, with its annotation), Now (current value, delta vs the previous period, trend per period) and Ahead (where the metric lands if the current trend continues, with a clearly labeled uncertainty band that is zero today and widens with every period). Give it timestamped history and it fits a straight-line or compound trend itself, or plug your own model with project(ctx); an optional target turns the forecast into a plain sentence (“Reaches $1M ARR around Jan 2028”, “Cash runs out around Feb 2028”, with the likely date range) and 1 to 3 app-defined levers (native range inputs: churn, weekly signups, spend cut, extra savings…) re-draw the future live, with the unchanged trend kept as a dotted ghost and the difference spelled out (“3 months sooner than on the current trend”, “instead of after Mar 2028”). The Ahead panel always states its assumptions in words, lines differ by dash pattern (solid actual, dashed projection, dotted current trend) and not only by color, and the layout switches from a split view to accessible tabs on narrow containers.

CategoryData display
TypeWeb Component (<mv-yet-to-come>)
Statusbeta
Also installsbutton, slider
Keywordsexclusive, culture, forecast, projection, trend, what-if, scenario, kpi, metrics, goal, target, runway, burn-rate, savings, sparkline, uncertainty, confidence-band, levers, dashboard, saas

When to use

Avoid when

Install

node scripts/add.mjs yet-to-come --out ./src/marvelous

AI agent with the Marvelous UI MCP server: install_components({ slugs: ["yet-to-come"], target_dir: "<absolute path>/src/marvelous", framework: "react" }).

Files copied (dependencies included): tokens/tokens.css, core/base.css, core/dom.js, core/element.js, core/focus.js, core/motion.js, core/observe.js, components/yet-to-come/yet-to-come.js, components/yet-to-come/yet-to-come.css, components/button/button.css, components/slider/slider.js, components/slider/slider.css.

Usage

Canonical markup — start from it and customize with attributes, data-* and CSS variables:

<div id="ytc-demo" style="width:min(100%,70rem);margin-inline:auto">
  <style>
    #ytc-demo { display:grid; gap:1.5rem }
    #ytc-demo .ytc-bar { display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:.75rem 1rem }
    #ytc-demo .ytc-bar h3 { margin:0; font-size:1.0625rem; letter-spacing:-.01em }
    #ytc-demo .ytc-bar p { margin:.125rem 0 0; color:var(--mv-fg-muted); font-size:.8125rem }
    #ytc-demo .ytc-actions { display:flex; flex-wrap:wrap; align-items:center; gap:.5rem .75rem }
    #ytc-demo .ytc-row { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:1.5rem; align-items:start }
    #ytc-demo .ytc-col { display:grid; gap:.75rem; min-width:0 }
    #ytc-demo .ytc-log { display:flex; align-items:center; gap:.5rem; color:var(--mv-fg-muted); font-size:.75rem; min-width:0 }
    #ytc-demo .ytc-log span:last-child { overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
    @media (max-width:52rem) { #ytc-demo .ytc-row { grid-template-columns:minmax(0,1fr) } }
  </style>

  <section class="ytc-col" aria-labelledby="ytc-arr-h">
    <div class="ytc-bar">
      <div>
        <h3 id="ytc-arr-h">Revenue outlook</h3>
        <p>Lumen Analytics · B2B plan at $149 per customer per month</p>
      </div>
      <div class="ytc-actions">
        <label class="mv-choice" data-control="end">
          <input type="checkbox" role="switch" class="mv-switch" id="ytc-band" checked>
          <span class="mv-choice-text"><span class="mv-choice-title">Range band</span></span>
        </label>
        <button class="mv-button" data-variant="outline" data-size="sm" type="button" id="ytc-close">Close October</button>
      </div>
    </div>
    <mv-yet-to-come id="ytc-arr" label="Annual recurring revenue" format="currency" target="1000000" target-label="$1M ARR" horizon="18" period="month">
      <script type="application/json" data-history>[
        ["2025-09-01", 712000], ["2025-10-01", 731000], ["2025-11-01", 744000], ["2025-12-01", 752000],
        ["2026-01-01", 761000], ["2026-02-01", 768000], ["2026-03-01", 801000, "Team plan launched"],
        ["2026-04-01", 812000], ["2026-05-01", 818000], ["2026-06-01", 822000], ["2026-07-01", 829000],
        ["2026-08-01", 834000], ["2026-09-01", 840360]
      ]</script>
      <script type="application/json" data-levers>[
        { "id": "churn", "label": "Monthly churn", "min": 0.01, "max": 0.05, "step": 0.001, "value": 0.03, "format": "percent", "decimals": 1 },
        { "id": "signups", "label": "New customers per week", "min": 1, "max": 8, "step": 0.1, "value": 3.7, "decimals": 1 }
      ]</script>
    </mv-yet-to-come>
    <div class="ytc-log" aria-hidden="true"><span class="mv-badge" data-variant="secondary" data-shape="pill" id="ytc-status">mv-projection</span><span id="ytc-last">Waiting for the first projection…</span></div>
  </section>

  <div class="ytc-row">
    <section class="ytc-col" aria-labelledby="ytc-cash-h">
      <div class="ytc-bar"><div><h3 id="ytc-cash-h">Runway</h3><p>Built-in straight-line trend, a limit to stay clear of</p></div></div>
      <mv-yet-to-come id="ytc-cash" label="Cash in the bank" format="currency" target="0" target-label="$0" target-mode="avoid" horizon="24">
        <script type="application/json" data-history>[
          ["2025-09-30", 2460000], ["2025-10-31", 2388000], ["2025-11-30", 2312000], ["2025-12-31", 2241000],
          ["2026-01-31", 2152000], ["2026-02-28", 2074000], ["2026-03-31", 1981000, "Two senior hires"],
          ["2026-04-30", 1897000], ["2026-05-31", 1809000], ["2026-06-30", 1724000], ["2026-07-31", 1638000],
          ["2026-08-31", 1553000], ["2026-09-30", 1471000]
        ]</script>
        <script type="application/json" data-levers>[
          { "id": "cut", "label": "Cut monthly spend by", "min": 0, "max": 40000, "step": 1000, "value": 0, "format": "currency", "effect": "add" },
          { "id": "bridge", "label": "Bridge round", "min": 0, "max": 750000, "step": 25000, "value": 0, "format": "currency", "effect": "shift" }
        ]</script>
      </mv-yet-to-come>
    </section>

    <section class="ytc-col" aria-labelledby="ytc-save-h">
      <div class="ytc-bar"><div><h3 id="ytc-save-h">Savings goal</h3><p>Weekly data, compact tabs layout</p></div></div>
      <mv-yet-to-come id="ytc-save" label="Emergency fund" format="currency" target="10000" horizon="26" panel="past" layout="tabs">
        <script type="application/json" data-history>[
          ["2026-06-07", 3120], ["2026-06-14", 3260], ["2026-06-21", 3410], ["2026-06-28", 3395, "Car repair"],
          ["2026-07-05", 3540], ["2026-07-12", 3700], ["2026-07-19", 3830], ["2026-07-26", 3990],
          ["2026-08-02", 4105], ["2026-08-09", 4280], ["2026-08-16", 4400], ["2026-08-23", 4560],
          ["2026-08-30", 4690], ["2026-09-06", 4850], ["2026-09-13", 4975], ["2026-09-20", 5140]
        ]</script>
        <script type="application/json" data-levers>[
          { "id": "extra", "label": "Extra per week", "min": 0, "max": 150, "step": 5, "value": 0, "format": "currency", "effect": "add", "description": "Set aside on top of your usual deposits." }
        ]</script>
      </mv-yet-to-come>
    </section>
  </div>

  <script type="module">
    await customElements.whenDefined("mv-yet-to-come");
    const arr = document.getElementById("ytc-arr");
    const cash = document.getElementById("ytc-cash");
    const PRICE = 149;

    // The app's own model: customers churn and sign up each month; ARR = 12 × MRR.
    arr.project = ({ history, levers, steps }) => {
      let customers = history.at(-1).v / (PRICE * 12);
      const perMonth = (levers.signups * 52) / 12;
      return {
        points: steps.map(() => {
          customers = customers * (1 - levers.churn) + perMonth;
          return customers * PRICE * 12;
        }),
        assumptions: `Every customer pays $${PRICE} per month; ARR is 12 times the monthly revenue of active customers.`,
      };
    };

    cash.strings = { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" };

    const status = document.getElementById("ytc-status");
    const last = document.getElementById("ytc-last");
    arr.addEventListener("mv-projection", (e) => {
      const { status: s, statement } = e.detail;
      status.textContent = s;
      status.dataset.variant = s === "reached" ? "success" : "secondary";
      last.textContent = statement;
    });

    document.getElementById("ytc-band").addEventListener("change", (e) => {
      for (const el of document.querySelectorAll("#ytc-demo mv-yet-to-come")) el.band = e.target.checked ? "true" : "false";
    });

    // New actuals arrive: the story moves one month forward.
    const close = document.getElementById("ytc-close");
    close.addEventListener("click", () => {
      const h = arr.history;
      const t = new Date(h.at(-1).t);
      t.setUTCMonth(t.getUTCMonth() + 1);
      arr.history = [...h, { t, v: h.at(-1).v + 6100 }];
      close.disabled = true;
      close.textContent = "October closed";
    });
  </script>
</div>

Cultural reference

A Christmas Carol — Charles Dickens (1843, book). Three spirits show a man his past, his present and the future that will come if nothing changes, and seeing that future is what makes him change course. The UI tells a metric the same way, past, now and the future on the current trend, and hands the user levers to watch that future change before it happens.

API

Attributes

NameTypeDefaultDescription
labelstringName of the metric (“Annual recurring revenue”), shown in the header and used as the accessible name of the group and of the tabs.
formatnumber | currency | percentnumberHow values are written (Intl.NumberFormat). Values of 10,000 and more are shown in compact notation ($840K, 1.2M). percent expects fractions (0.42 = 42%).
currencyISO 4217 codeUSDCurrency for format="currency" (also the default of currency levers).
unitstringSuffix appended to every value (“t CO₂e”, “GB”, “days”). Ignored for percent.
decimalsnumberFixed number of decimals for values under 10,000 (default: automatic).
localeBCP 47 tagen-USLocale for numbers and dates (dates are read and written in UTC).
periodday | week | month | quarter | yearStep of the series and of the projection (months, quarters and years follow the calendar). Guessed from the median gap between history points when omitted.
horizonnumber (periods)How many periods ahead to project. Defaults to the length of the history (3 to 60).
fit-windownumber (points)Only the last N history points feed the built-in trend and the range (e.g. 6 to follow a recent change of pace). Default: all.
modellinear | exponential | autolinearBuilt-in trend: a straight line (least squares, continuing from today’s actual value) or compound growth (log-linear fit, positive values only). auto keeps compound growth only when it fits clearly better. Also sizes the range band when project() is used.
targetnumberGoal or limit. The Ahead panel then says when the projection crosses it (interpolated between periods), the likely date range from the band, and draws a target line with a crossing marker.
target-labelstringWording of the target in sentences and on the chart (“$1M ARR”, “$0”). Default: the formatted value.
target-modereach | avoidreachreach: crossing is the goal (“Reaches … around …”, success outcome). avoid: crossing is the danger (“Hits … around …”, danger outcome; “Stays clear of … through …” otherwise), for runway, budgets and quotas.
band"true" | "false"true"false" hides the uncertainty band, its legend entry, the date range and its sentence in the assumptions.
confidencenumber (0.5..0.99)0.8Coverage of the band. It is a random-walk-with-drift range: the spread of past period-to-period changes around the trend, zero today and widening with the square root of the distance plus the uncertainty on the trend itself.
layoutauto | split | tabs | stackautosplit: Past and Now stacked on the left, Ahead on the right. tabs: one panel at a time behind an ARIA tablist. stack: all three in one column. auto picks tabs when the element is narrower than 640px, split otherwise.
panelpast | present | futurefutureSelected panel in the tabs layout (reflects the user’s choice).
heading-level1..63Level of the three panel headings, to fit the page outline.
data-layout / data-reveal / data-changed / data-pendingset by the componentResolved layout; story reveal state (pending until first seen, then done); present while a lever differs from its starting value; present while an async project() is running (aria-busy is set too).

Properties

NameTypeDescription
historyArray<[t, v, note?] | { t, v, note? }>Timestamped values (Date, ISO string or epoch ms). Sorted for you; a note marks the point in the Past panel and names the biggest jump when it falls on it. Can also come from a child <script type="application/json" data-history>. Reading it returns [{ t: Date, v, note }].
leversArray<Lever>Lever = { id, label, min = 0, max = 100, step, value, format?: "number" | "currency" | "percent" | (v) => string, decimals?, unit?, currency?, description?, assumption? (template with {label} {value} {base}), effect? }. value "auto" starts a rate lever at the fitted trend. effect (built-in model only): add (value added per period, default), rate (replaces the trend per period, or the growth rate for exponential), scale (multiplies the trend), shift (one-time amount added from the next period). Also from <script type="application/json" data-levers>. Setting them resets their values.
valuesRecord<string, number>Current lever values. Setting merges and clamps (e.g. restore a saved scenario); it re-projects and announces like a user change.
project(ctx) => number[] | Point[] | { points, assumptions? } | Promise<…>Your model. ctx = { history, levers (values by id), steps (Date of each future period), period, horizon, baseline (true when computing the unchanged scenario), fit: { model, rate, slope } }. Return one value per step (or { v, lo?, hi? } to supply your own range; missing ranges are sized by the built-in fit). assumptions is a sentence prepended to the lever list. Async results show a pending state; stale ones are dropped.
momentsArray<{ t, label }>Extra annotated moments (a launch, a price change) marked on the sparkline; the one inside the biggest jump names it. Also from <script type="application/json" data-moments>.
formatter(value, { compact, signed, precise }) => stringReplaces format / currency / unit / decimals for every value.
stringsPartial<Record<string, string>>Overrides for every visible text and announcement, with {placeholders} (e.g. { avoidHit: "Cash runs out around {date}", avoidClear: "Cash lasts past {end}" }). Keys: past, present, future, futureIdle, futureChanged, tabs, empty, pastUp, pastDown, pastFlat, momentUp, momentDown, onDate, inDate, asOf, versusPrev, trend, perPeriod, reach, notReached, already, avoidHit, avoidClear, endpoint, rangeDates, rangeOpen, rangePossible, rangeValue, sooner, later, same, insteadOfNever, insteadOf, versus, assumptions, assumeLinear, assumeExp, assumeCustom, assumeLever, assumeLeverChanged, assumeBand, assumeNoBand, levers, reset, leverNow, legendActual, legendProjection, legendBand, legendBaseline, legendTarget, now, projected, tablePast, tableFuture, tableDate, tableValue, tableRange. English defaults.
projectionobject | nullLatest result, read-only: the same object as mv-projection’s detail.
label / format / target / horizon / …reflectedEvery attribute has a matching property (camelCase: fitWindow, targetLabel, targetMode, headingLevel).

Methods

NameDescription
setLever(id, value)Sets one lever (clamped to its range) and re-projects.
resetLevers()Puts every lever back to its starting value (the “Reset” button does the same).
refresh()Re-runs the fit and the projection, e.g. after inputs of your project() changed outside the levers.
show(panel)Selects "past", "present" or "future" in the tabs layout.

Events

NameDescription
mv-leverCancelable, on every input of a lever. detail: { id, value, previous, values }. preventDefault() puts the input back (e.g. a value your plan does not allow).
mv-projectionAfter every projection, including the first. detail: { points: [{ t: Date, v, lo, hi }] (index 0 = today’s actual value), baseline (the unchanged-trend points when a lever moved, else null), status: "reached" | "not-reached" | "already" | "none", crossing: { date, earliest, latest, periods } | null, statement, range, assumptions, changed, levers }.

Content structure

NameDescription
script[data-history] / [data-levers] / [data-moments]Optional JSON children for plain-HTML use; the component’s own DOM is appended after them.

CSS classes

NameDescription
mv-yet-to-come-root / -header / -label / -tabs / -tab / -panelsCard frame, header with the metric label and the tablist (tabs layout only).
mv-yet-to-come-panel[data-panel]past | present | future panels; -panel-head, -title (hollow, filled or dashed marker), -sub, -asof.
mv-yet-to-come-spark / -summary / -momentPast panel: sparkline with start, end, note and key-moment dots; summary sentences.
mv-yet-to-come-value / -delta[data-direction] / -trendNow panel: big value, delta chip with arrow icon and sign, trend per period.
mv-yet-to-come-statement[data-outcome] / -outcome / -range / -chart / -plot / -legend / -assumptionsAhead panel: outcome sentence (good | bad | neutral, with an icon), date or value range, chart (-history, -projection, -baseline, -band, -target-line, -now-line paths; -dot[data-kind], -target-label, -cross-label, -chip hover readout), legend and assumptions.
mv-yet-to-come-levers / -lever[data-changed] / -lever-input / -lever-notchLever group; each lever is a native <input type="range" class="mv-slider">, and the notch marks its starting value once moved.

CSS variables

NameDefaultDescription
--mv-yet-to-come-actualvar(--mv-fg)Color of the actual (past) line and its area.
--mv-yet-to-come-projectionvar(--mv-accent)Color of the projection, band edges, Ahead tint and changed lever values.
--mv-yet-to-come-bandprojection at 15%Fill of the uncertainty band.
--mv-yet-to-come-targetvar(--mv-fg-muted)Target line and label.
--mv-yet-to-come-good / -badvar(--mv-success) / var(--mv-danger)Outcome colors (always paired with an icon and words).
--mv-yet-to-come-chart-height13remHeight of the Ahead chart.
--mv-yet-to-come-paddingvar(--mv-space-5)Inner padding of the panels.

Accessibility

The element is a labelled group (aria-labelledby the metric label) and every panel carries a real heading (heading-level) plus a text summary that stands on its own: Past says where the metric went and its biggest move, Now gives the value, the delta with its sign and the trend in words, Ahead gives the outcome sentence, the range and the assumptions. Charts are SVG with aria-hidden; each has a visually hidden table equivalent (date, value, and the range for projected points, sampled to 24 rows), and the hover readout is a pointer-only extra. Levers are native <input type="range"> elements with a <label>, an optional description linked by aria-describedby, and aria-valuetext set to the formatted value (“2.0%”), so arrows, Page Up/Down and Home/End work everywhere; the visible value is aria-hidden to avoid double reading. Projection changes are announced once in a polite live region, 700 ms after the last lever move (“Reaches $1M ARR around Jan 2028, instead of after Mar 2028. As early as …”), never on the initial render. In the tabs layout the panel switcher is a proper tablist (roving tabindex, arrows, Home/End, automatic activation, aria-controls/aria-selected) and panels are tabpanels labelled by their tab; in the split and stack layouts the panels are labelled groups. Nothing relies on color alone: actual, projection and current trend are solid, dashed and dotted lines (the legend repeats the patterns), the outcome has an icon and words, the delta has an arrow and a sign, panel markers differ by shape. The async state sets aria-busy. Reduced motion (OS or data-motion="reduce"): no story reveal, no line glide, tab panels switch instantly. Forced colors: lines map to CanvasText, Highlight and GrayText, the band keeps its dashed edges, the selected tab gets a Highlight outline.