Back to Widget Patterns
v2.3Interactive Tooling
Savings Calculator
Generic config-driven calculator. You define input fields and a compute function via props; the component handles state, formatting, and real-time result display. Covers ROI calculations and savings projections.
Live preview
Rendered with real component code. Each example demonstrates a documented variant.
SaaS migration savings
Estimate your migration savings
Based on typical results from teams that switched to a lower-cost stack.
Monthly savings$1,500
Annual savings$18,000
Savings per seat / year$1,800
ROI calculator
Project your revenue growth
Enter your current revenue and an expected growth rate to see the year 1 projection.
Year 1 projected revenue$600,000
Revenue gained$100,000
Growth rate applied20%
React props
Configure the component via props in React or via attributes in HTML.
| Prop | Type | Default | Description |
|---|---|---|---|
| title | string | undefined | Calculator heading |
| description | string | undefined | Supporting text below the heading |
| inputs | InputDef[] | required | Array of input field configs ({ name, label, type, defaultValue?, prefix?, suffix?, options?, min?, max?, step? }) |
| compute | (values: Record<string, string>) => Result[] | required | Computation function called on every input change; returns an array of formatted result rows |
| layout | "stacked" | "side-by-side" | "stacked" | Inputs and results layout; side-by-side places them in two columns on wider screens |
| className | string | undefined | Extra class applied to the root element |
HTML usage
The HTML variant uses the same shared styles and class names. Drop into any stack.
<link rel="stylesheet" href="path/to/savings-calculator/styles.css">
<section class="sc">
<h2 class="sc__title">Calculate your annual savings</h2>
<form class="sc__inputs" id="my-calc" novalidate>
<label class="sc__field">
<span class="sc__label">Current monthly spend</span>
<div class="sc__input-group">
<span class="sc__prefix">$</span>
<input class="sc__input" type="number" name="currentSpend" value="5000">
</div>
</label>
<label class="sc__field">
<span class="sc__label">Team size</span>
<input class="sc__input" type="number" name="teamSize" value="10">
</label>
</form>
<div class="sc__results" aria-live="polite" data-results>
<!-- Populated by script -->
</div>
</section>
<script>
(function() {
var form = document.getElementById('my-calc');
var results = document.querySelector('[data-results]');
function compute(values) {
var monthlySavings = Number(values.currentSpend) * 0.3;
var annualSavings = monthlySavings * 12;
return [
{ label: 'Monthly savings', value: monthlySavings, format: 'currency' },
{ label: 'Annual savings', value: annualSavings, format: 'currency', emphasized: true },
];
}
function format(value, type) {
if (type === 'currency') return '$' + Number(value).toLocaleString('en-US', { maximumFractionDigits: 0 });
if (type === 'months') return value + ' months';
if (type === 'percent') return value + '%';
return String(value);
}
function render() {
var formData = new FormData(form);
var values = Object.fromEntries(formData);
var computed = compute(values);
results.innerHTML = computed.map(function(r) {
return '<div class="sc__result' + (r.emphasized ? ' sc__result--emphasized' : '') + '">'
+ '<span class="sc__result-label">' + r.label + '</span>'
+ '<span class="sc__result-value">' + format(r.value, r.format) + '</span>'
+ '</div>';
}).join('');
}
form.addEventListener('input', render);
render();
})();
</script>Customization
Override these CSS custom properties to integrate the component into your brand.
.sc {
--sc-bg: var(--brand-surface, white);
--sc-text-color: var(--brand-ink, #102542);
--sc-muted-color: rgba(16, 37, 66, 0.6);
--sc-input-border: rgba(0, 0, 0, 0.12);
--sc-input-focus-ring: var(--brand-accent, #1e5fcf);
--sc-input-padding: 0.75rem;
--sc-result-bg: rgba(0, 0, 0, 0.025);
--sc-result-emphasized-bg: var(--brand-accent, #1e5fcf);
--sc-result-emphasized-text: white;
--sc-gap: 1rem;
--sc-section-padding: 2rem;
--sc-radius: 1rem;
--sc-title-size: 1.5rem;
}