Skip to content

Inputs

3 min read · Updated 28 Oct 2026

Inputs let the reader set a value and see the page recalculate: a price change, a discount, a commission rate. A value reaches the data as a parameter of a measure, which changes what the measure calculates, or as a selection on a field, which changes which rows count. Inputs sit in the filter bar, in step 1 of a numbered-steps page, or beside the chart they drive.

Inputs: A parameter input changes a declared what-if calculation.

Illustrative example with synthetic data. A parameter input changes a declared what-if calculation.

For a pick from a list use a filter object or quick filters; for a period use the period picker.

  1. Does the value change what a measure calculates, such as a price or a rate? Use a parameter input.
  2. Does it narrow rows to a range of a numeric or date field? Use a range selection.
  3. Is it on or off? Use a toggle, sent as a parameter if a measure declares it, otherwise as a selection.
Variant Control Use cases Choose another when
Parameter input Native range or number input Projected revenue at a price change of −20% to +20%; payout at a commission rate of 2–12% The value selects rows
Range selection The slider object, or a native input writing a selection Orders over a value threshold; customers by tenure band The value enters a formula
Toggle Checkbox with role="switch" Include returns in net sales; show amounts with tax There are more than two states
<fieldset class="inputs">
<legend>Assumptions</legend>
<div class="input">
<label for="price-change">Price change</label>
<output class="input__value" for="price-change" id="price-change-value">+5%</output>
<input type="range" id="price-change" min="-20" max="20" step="1" value="5" aria-valuetext="+5%" />
<span class="input__range" aria-hidden="true"><span>−20%</span><span>+20%</span></span>
</div>
<div class="input">
<label for="commission">Commission rate (%)</label>
<input type="number" id="commission" min="2" max="12" step="0.5" value="6" inputmode="decimal" />
</div>
<div class="input input--toggle">
<label for="include-returns">Include returns</label>
<input type="checkbox" role="switch" id="include-returns" />
</div>
<button type="button" class="inputs__reset">Reset assumptions</button>
</fieldset>
  • <output> shows the current value with its unit; aria-valuetext gives assistive technology the same text. A number input shows its own value, so put the unit in its label.
  • role="switch" reads as on and off rather than checked.

The inputs read --muted, --line, --accent and --radius from the style. Each input has a fixed height, so a value that grows from 5% to −20% does not move its neighbours.

:root {
/* 16 label + 4 + 24 control + 4 + 16 range = 64 */
--input-label-h: 16px;
--input-control-h: 24px;
--input-range-h: 16px;
--input-h: calc(var(--input-label-h) + var(--input-control-h) + var(--input-range-h) + 8px);
--input-min: 200px;
}
/* One track needs 200px; two need 2 × 200 + 16 = 416px. */
.inputs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(var(--input-min), 1fr));
gap: var(--gap);
min-width: 0;
margin: 0;
padding: 0;
border: 0;
}
.input {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
grid-template-rows: var(--input-label-h) var(--input-control-h) var(--input-range-h);
row-gap: 4px;
height: var(--input-h);
min-width: 0;
}
.input > label {
overflow: hidden;
line-height: var(--input-label-h);
text-overflow: ellipsis;
white-space: nowrap;
}
.input__value {
/* Tabular figures and a minimum width, so the value does not shift as it changes. */
min-width: 5ch;
font-variant-numeric: tabular-nums;
line-height: var(--input-label-h);
text-align: end;
}
.input > input {
grid-column: 1 / -1;
box-sizing: border-box;
width: 100%;
height: var(--input-control-h);
margin: 0;
accent-color: var(--accent);
}
.input__range {
grid-column: 1 / -1;
display: flex;
justify-content: space-between;
color: var(--muted);
line-height: var(--input-range-h);
}
.input > input[type='number'] {
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 0 8px;
}
.input--toggle > input {
justify-self: start;
width: auto;
}

accent-color colours the native range and checkbox from the style’s accent. In the filter bar, give inputs the bar’s --filter-h instead of --input-h, so they line up with the filter hosts. A narrow range input is hard to set precisely; pair it with a number input or widen --input-min.

A measure can declare a parameter for its formula or filter. An app has no Workbook variables, so the page sends the value:

  • To a query you build, on the calculation that declares it; see Parameters.
  • To a mounted object’s measure, with setMeasureDefinedParameter and a static value. Most types publish it; check the type’s page, for example bar. Address the measure with the two keys compilation.resolvedRefs returned for its ref, dataItemInternalKey and dataItemUniqueSignature, and nothing else: the payload rejects any other property.
// runtime and availableFields as in Mounting objects.
declare const targets: { instanceId: string; ref: { dataItemInternalKey: string; dataItemUniqueSignature: string } }[];
declare const priceChangeName: string; // the parameter's name as the measure exposes it
declare function refreshFigures(): void; // your own queries, sending the same value
declare function checkResults(results: unknown): void; // as in Check operation results
declare function markFailed(id: string, error: unknown): void; // shows the object's error state
// One chain, so a later value is always applied after an earlier one.
let chain: Promise<void> = Promise.resolve();
const setPriceChange = (value: { type: 'static'; value: number } | { type: 'default' }) => {
chain = chain
.catch(() => undefined)
.then(async () => {
for (const { instanceId, ref } of targets) {
try {
const applied = await applyRuntimeOperations({
runtime,
instanceId,
availableFields,
operationRequests: [
{
id: 'price-change',
name: 'setMeasureDefinedParameter',
payload: {
dataItemInternalKey: ref.dataItemInternalKey,
dataItemUniqueSignature: ref.dataItemUniqueSignature,
parameterName: priceChangeName,
value,
},
},
],
});
checkResults(applied.results);
} catch (error) {
// The other objects take the new value, so this one can no longer match the input.
markFailed(instanceId, error);
}
}
refreshFigures();
});
return chain;
};
  • { type: 'default' } removes the override; use it for Reset.
  • The operation changes the instance’s vSpec in memory. After a reload the object starts from its definition, so keep the value in your app’s state.

A range selection narrows rows to an interval, and every object in the group follows it. Use the slider object for a range on a numeric or date field: it writes the selection itself. Configure it with slider operations such as setSliderRange, setSliderStep and setSliderLayout, and turn setSliderQuickSelect off on a page with an apply bar.

Use a native input when the value needs its own label, unit and formatting, and write the selection yourself; see Driving selections from your own UI:

declare const GROUP: string;
declare const orderValueKey: string; // looked up by name
const selections = app.client.services.selections;
const selectOrderValue = (min: number, max: number) =>
selections.selectValues(GROUP, { field: { fieldKey: orderValueKey }, operator: 'between', values: [min, max] });

A range input fires input for every step and change when the reader releases it. Update the displayed value on input; send on change, debounced, so a run of arrow-key presses sends once.

declare function formatPercentChange(value: number): string; // with the app's one locale
const range = document.querySelector<HTMLInputElement>('#price-change');
const output = document.querySelector<HTMLOutputElement>('#price-change-value');
if (!range || !output) throw new Error('Price change input not found.');
let timer: ReturnType<typeof setTimeout> | undefined;
let sent = range.valueAsNumber;
range.addEventListener('input', () => {
const text = formatPercentChange(range.valueAsNumber);
output.value = text;
range.setAttribute('aria-valuetext', text);
});
range.addEventListener('change', () => {
clearTimeout(timer);
timer = setTimeout(() => {
const value = range.valueAsNumber;
if (Number.isNaN(value) || value === sent) return;
sent = value;
setPriceChange({ type: 'static', value }).catch((error: unknown) =>
console.error('Price change not applied', error),
);
}, 300);
});
  • A committed parameter costs one query per object that uses it; a selection costs one query per mounted object in the group when it applies.
  • Listen for change on a number input too; it fires when a typed value is committed. Do not send NaN from an empty field, or a value outside min and max.
  • Send a toggle on change; it needs no debounce.
  • Choose step so the keyboard reaches useful values in few presses; Home and End reach min and max.