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.

PropTypeDefaultDescription
titlestringundefinedCalculator heading
descriptionstringundefinedSupporting text below the heading
inputsInputDef[]requiredArray of input field configs ({ name, label, type, defaultValue?, prefix?, suffix?, options?, min?, max?, step? })
compute(values: Record<string, string>) => Result[]requiredComputation 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
classNamestringundefinedExtra 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;
}