Data display
Term-by-term derivation
A computed figure shown with the audit trail behind it: the formula once with the live inputs already substituted, then one row per term carrying its value and a sentence about what that term does — collapsed by default, because the number is the product and the derivation is the receipt.
Reach for it when building
- a pricing or quote breakdown
- a loan or amortization calculator
- a score, rating, or ranking readout
- a tax or fee estimate
- a game damage or catch-rate calculator
- any figure people email support about
- explainability
- calculator
- disclosure
- formula
- tabular-nums
- details
<div class="tbt-demo">
<label class="tbt-field">
<span>Remaining HP</span>
<input type="range" id="tbt-hp" min="1" max="100" value="18" />
<output class="tbt-out" id="tbt-hp-out">18%</output>
</label>
<p class="tbt-answer" id="tbt-answer"></p>
<details class="tbt-details">
<summary>How this number was reached</summary>
<p class="tbt-formula" id="tbt-formula"></p>
<table class="tbt-table">
<caption class="tbt-caption">Every term, with what it does</caption>
<thead>
<tr><th scope="col">Term</th><th scope="col">Value</th><th scope="col">What it does</th></tr>
</thead>
<tbody id="tbt-rows"></tbody>
</table>
</details>
</div>Colors come from shared theme tokens — --surface, --ink, --border, --accent and friends — so this CSS carries no palette
of its own. Use Runnable file to copy the tokens along with it.
.tbt-demo { display: flex; flex-direction: column; gap: 0.75rem; max-width: 560px; }
.tbt-field { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-size: 0.9375rem; color: var(--dim); }
.tbt-field input[type='range'] { flex: 1 1 12rem; min-height: 44px; accent-color: var(--accent); }
.tbt-out { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--ink); font-weight: 600; }
.tbt-answer {
margin: 0;
font-family: var(--mono);
font-variant-numeric: tabular-nums;
font-size: 1.25rem;
color: var(--ink);
}
.tbt-details {
border: 1px solid var(--border);
border-radius: var(--radius);
background: var(--surface);
padding: 0.75rem 0.875rem;
}
.tbt-details summary { cursor: pointer; font-weight: 600; }
.tbt-details[open] summary { margin-bottom: 0.75rem; }
/* The symbolic form appears once, at the top, with the live values already in
it — never repeated per row. */
.tbt-formula {
margin: 0 0 0.75rem;
font-family: var(--mono);
font-size: 0.9375rem;
color: var(--dim);
overflow-x: auto;
white-space: nowrap;
padding-bottom: 0.25rem;
}
.tbt-table { width: 100%; border-collapse: collapse; font-size: 0.9375rem; }
.tbt-caption { text-align: left; color: var(--dim); font-size: 0.9375rem; padding-bottom: 0.5rem; }
.tbt-table th,
.tbt-table td { text-align: left; padding: 0.4rem 0.5rem; border-bottom: 1px solid var(--border); vertical-align: top; }
.tbt-table th { color: var(--dim); font-weight: 600; }
.tbt-value { font-family: var(--mono); font-variant-numeric: tabular-nums; white-space: nowrap; }
/* The result row is the one that ties back to the headline figure. */
.tbt-table tr[data-role='result'] td { border-bottom: none; font-weight: 600; color: var(--ink); }const MAX_HP = 160;
const CATCH_RATE = 45;
const BALL_BONUS = 1.5;
const STATUS_BONUS = 1.5;
const THRESHOLD = 255;
const slider = document.getElementById('tbt-hp');
const hpOut = document.getElementById('tbt-hp-out');
const answer = document.getElementById('tbt-answer');
const formula = document.getElementById('tbt-formula');
const rows = document.getElementById('tbt-rows');
function render() {
const fraction = Number(slider.value) / 100;
const current = Math.max(1, Math.round(MAX_HP * fraction));
const hpTerm = (3 * MAX_HP - 2 * current) / (3 * MAX_HP);
const modified = hpTerm * CATCH_RATE * BALL_BONUS * STATUS_BONUS;
const chance = Math.min(1, modified / THRESHOLD);
hpOut.textContent = slider.value + '%';
answer.textContent = 'Catch chance ' + (chance * 100).toFixed(1) + '% per throw';
/* Substituted, not symbolic. Someone checking the maths should not have to
look up what each letter stood for. */
formula.textContent =
'a = ((3 × ' + MAX_HP + ' − 2 × ' + current + ') ÷ (3 × ' + MAX_HP + ')) × ' +
CATCH_RATE + ' × ' + BALL_BONUS + ' × ' + STATUS_BONUS;
const terms = [
{
name: 'HP term',
value: hpTerm.toFixed(3),
why: 'Maxes at 1 near zero HP and bottoms out at one third at full health, so chipping health away matters more than any item.'
},
{
name: 'Species catch rate',
value: String(CATCH_RATE),
why: 'Fixed per species. A low number here caps everything downstream no matter what you throw.'
},
{
name: 'Ball bonus',
value: BALL_BONUS.toFixed(1),
why: 'Multiplies the whole expression, so it is worth the same half again at any HP.'
},
{
name: 'Status bonus',
value: STATUS_BONUS.toFixed(1),
why: 'Sleep and freeze pay more than paralysis, and they stack with nothing else.'
},
{
name: 'Modified rate',
value: modified.toFixed(1),
why: 'Compared against ' + THRESHOLD + '. At or above it the catch is guaranteed and no throw is wasted.',
role: 'result'
}
];
rows.innerHTML = '';
for (const term of terms) {
const row = document.createElement('tr');
if (term.role) row.dataset.role = term.role;
row.innerHTML = '<td></td><td class="tbt-value"></td><td></td>';
const cells = row.querySelectorAll('td');
cells[0].textContent = term.name;
cells[1].textContent = term.value;
cells[2].textContent = term.why;
rows.append(row);
}
}
slider.addEventListener('input', render);
render();Paste this into an agent to rebuild the pattern from scratch.
Build a computed figure that can show its own working. The answer sits in the open. Behind a disclosure, the formula appears once with the live inputs already substituted, followed by one row per term: the term, its current value, and a sentence about what that term does.
Reach for this wherever a number causes arguments — a quote, a fee, a score, an interest total, a damage or drop-rate calculation. These are the figures people screenshot and send to support. Walk away when the arithmetic is one visible step, like a subtotal above a total; a derivation table for `price × quantity` is condescending.
The rule that makes it work: **the third column is a sentence about the term, not a restatement of the arithmetic.** "Maxes at 1 near zero HP and bottoms out at one third at full health" teaches someone how to act. "(3·maxHP − 2·HP) ÷ 3·maxHP" in prose teaches nothing that the formula above did not already say, and it is the version that gets written by default.
**Substitute the live values into the formula, and print it once.** The symbolic form belongs at the top of the section, with the current numbers already in place so nobody has to hold a legend in their head. Repeating it per row is how the table stops being readable.
Collapse the derivation by default. The number is the product; the derivation is the receipt. Keep it in a native `details` so it is keyboard-operable, findable by in-page search in most browsers, and open to deep linking.
Order the rows by the order of the computation, and mark the result row differently — that is the one that ties back to the headline figure. If the result is compared against a threshold, name the threshold and say what crossing it means in the same sentence.
Set every figure in tabular numerals with the value column left-aligned but nowrap. A derivation table whose digits do not line up undoes the point of showing the terms at all.
Recompute the whole table from the same inputs the headline uses — never from a cached copy. Two sources for one number is how a page ends up displaying a figure its own explanation contradicts. Every colour comes from theme custom properties.