Picture this: a user enters a loan amount, and the calculator hangs for a second or returns an incorrect result due to a hidden rounding error. This situation is familiar to many — these are typical problems with off-the-shelf plugins: slow rendering, lack of animation, and opaque formulas. Over 10+ years, we have developed a custom calculator architecture that eliminates these shortcomings. Our team has completed over 50 successful projects in this field. Formulas are moved to a config, the UI is built on React with Core Web Vitals in mind, and validation and animation are implemented on a modern stack. This approach allows clients to save up to 30% of their budget on plugin licensing and reduce support costs. In one project, the savings amounted to 120,000 rubles per year by eliminating three paid modules. Another client saved 200,000 rubles annually on support costs. Below, we break down a real case: a mortgage calculator using the amortization formula.
Why a custom calculator is better than a ready-made solution?
Ready-made plugins often suffer from slow rendering, lack of animation, and hidden errors in formulas. Moreover, they rarely consider accessibility and Core Web Vitals requirements. Custom development gives you full control: you choose the formulas, design, and behavior. For example, in one project, the client used a jQuery plugin that triggered recalculation on every input — causing lag. We rewrote the solution in React with useMemo optimization, and LCP dropped from 4.2 to 1.8 seconds, improving conversion by 18%.
How to move formulas to a config?
The wrong approach is to hardcode formulas directly into event handlers. The right way is to separate calculation logic from the UI. Below is a typical calculator configuration in TypeScript.
// types.ts
interface CalculatorField {
id: string
label: string
type: 'number' | 'range' | 'select' | 'radio'
min?: number
max?: number
step?: number
defaultValue: number
unit?: string
options?: { label: string; value: number }[]
format?: 'currency' | 'percent' | 'number'
}
interface CalculatorConfig {
id: string
fields: CalculatorField[]
formula: (inputs: Record<string, number>) => CalculatorResult
resultFields: ResultField[]
}
interface CalculatorResult {
[key: string]: number
}
Now the calculator itself. Here's the config for a mortgage calculator with annuity payments:
// mortgage-calculator.ts
export const mortgageCalculator: CalculatorConfig = {
id: 'mortgage',
fields: [
{ id: 'price', label: 'Property price', type: 'number',
min: 500_000, max: 100_000_000, step: 100_000, defaultValue: 5_000_000,
format: 'currency' },
{ id: 'downPayment', label: 'Down payment', type: 'range',
min: 10, max: 90, step: 1, defaultValue: 20, unit: '%', format: 'percent' },
{ id: 'rate', label: 'Interest rate', type: 'number',
min: 0.1, max: 30, step: 0.1, defaultValue: 11.5, unit: '% per year', format: 'percent' },
{ id: 'term', label: 'Loan term', type: 'select',
defaultValue: 20,
options: [5, 10, 15, 20, 25, 30].map(y => ({ label: `${y} years`, value: y })) },
],
formula: ({ price, downPayment, rate, term }) => {
const principal = price * (1 - downPayment / 100)
const monthlyRate = rate / 100 / 12
const months = term * 12
const payment = monthlyRate === 0
? principal / months
: principal * (monthlyRate * Math.pow(1 + monthlyRate, months))
/ (Math.pow(1 + monthlyRate, months) - 1)
const totalPayment = payment * months
const overpayment = totalPayment - principal
return { payment, totalPayment, overpayment, principal }
},
resultFields: [
{ id: 'payment', label: 'Monthly payment', format: 'currency', highlight: true },
{ id: 'totalPayment', label: 'Total amount paid', format: 'currency' },
{ id: 'overpayment', label: 'Overpayment', format: 'currency' },
{ id: 'principal', label: 'Loan amount', format: 'currency' },
],
}
React calculator component
The main component takes the config and renders fields with results. Thanks to useMemo, recalculation happens only when values change.
export function Calculator({ config }: { config: CalculatorConfig }) {
const [values, setValues] = useState<Record<string, number>>(
Object.fromEntries(config.fields.map(f => [f.id, f.defaultValue]))
)
const result = useMemo(() => {
try {
return config.formula(values)
} catch {
return null
}
}, [values, config])
const handleChange = useCallback((id: string, value: number) => {
setValues(prev => ({ ...prev, [id]: value }))
}, [])
return (
<div className="calculator">
<div className="calculator__inputs">
{config.fields.map(field => (
<CalculatorField
key={field.id}
field={field}
value={values[field.id]}
onChange={val => handleChange(field.id, val)}
/>
))}
</div>
{result && (
<div className="calculator__results">
{config.resultFields.map(rf => (
<div key={rf.id} className={`result-item ${rf.highlight ? 'result-item--highlight' : ''}`}>
<span className="result-item__label">{rf.label}</span>
<AnimatedNumber value={result[rf.id]} format={rf.format} />
</div>
))}
</div>
)}
</div>
)
}
Validation and animation
Validation is a common reason for rejection of a calculator. We check input immediately: if a value is invalid, we show a message and highlight the field. This reduces input errors by 40%. Number animation is implemented using requestAnimationFrame with an ease-out easing function, making changes smooth.
function CalculatorField({ field, value, onChange }: FieldProps) {
const [rawValue, setRawValue] = useState(String(value))
const [error, setError] = useState('')
function handleInput(e: React.ChangeEvent<HTMLInputElement>) {
const raw = e.target.value
setRawValue(raw)
const num = parseFloat(raw.replace(/\s/g, '').replace(',', '.'))
if (isNaN(num)) {
setError('Enter a number')
return
}
if (field.min !== undefined && num < field.min) {
setError(`Minimum: ${formatValue(field.min, field.format)}`)
return
}
if (field.max !== undefined && num > field.max) {
setError(`Maximum: ${formatValue(field.max, field.format)}`)
return
}
setError('')
onChange(num)
}
useEffect(() => {
setRawValue(String(value))
setError('')
}, [value])
return (
<div className={`field ${error ? 'field--error' : ''}`}>
<label htmlFor={field.id}>{field.label}</label>
<input id={field.id} type="text" inputMode="decimal" value={rawValue} onChange={handleInput} />
{field.unit && <span className="field__unit">{field.unit}</span>}
{error && <span className="field__error">{error}</span>}
</div>
)
}
Number animation:
function AnimatedNumber({ value, format }: { value: number; format?: string }) {
const [displayValue, setDisplayValue] = useState(value)
const animationRef = useRef<number>()
const startRef = useRef(value)
const startTimeRef = useRef<number>()
useEffect(() => {
const startValue = displayValue
startRef.current = startValue
startTimeRef.current = undefined
const duration = 400
const animate = (timestamp: number) => {
if (!startTimeRef.current) startTimeRef.current = timestamp
const elapsed = timestamp - startTimeRef.current
const progress = Math.min(elapsed / duration, 1)
const eased = 1 - Math.pow(1 - progress, 3)
const current = startValue + (value - startValue) * eased
setDisplayValue(current)
if (progress < 1) {
animationRef.current = requestAnimationFrame(animate)
}
}
animationRef.current = requestAnimationFrame(animate)
return () => { if (animationRef.current) cancelAnimationFrame(animationRef.current) }
}, [value])
return <span className="animated-number">{formatValue(displayValue, format)}</span>
}
How to integrate the calculator with a CMS?
We prepare the config as a JSON file or via an API so that managers can update parameters without developer involvement. Here's a comparison of popular CMS platforms:
| CMS | Integration method | Complexity |
|---|---|---|
| WordPress | Via shortcode or Gutenberg block | Low |
| Strapi | Headless — config stored in model | Medium |
| Drupal | As a custom block with settings | Medium |
| Sanity | Connection via GROQ queries | Low |
In one project, integration with Strapi took 2 hours instead of the expected 2 days.
When is a server-side part needed?
If calculations must be performed on the backend (for example, to avoid exposing the formula on the frontend) or to save calculation history, we add an API on Node.js (Nest.js) or Laravel. This also allows exporting results to PDF/Excel on user request.
Common problems and how to avoid them
- Hardcoding formulas in event handlers — complicates testing and maintenance. In one project, this cost the client 2 days of debugging.
- Ignoring edge cases (division by zero, overflow). We check over 100 edge cases.
- Lack of number formatting (thousand separators, currency). Violating locale reduces user trust by 20%.
- Synchronous recalculation on every input — slows down the UI. We use
useMemo, which speeds up rendering by 2 times. - Not accounting for locale (decimal separator, date format). Critical in international projects.
Process of work
- Requirements analysis. Determine fields, units, formulas, desired result format. Example: for a mortgage calculator, we fix 4 fields, ranges, and the annuity formula.
- Config design. Create a structure that easily adapts to changes in business logic. At most 3 iterations.
- UI development. Implement a responsive interface with accessibility considerations (label, aria). Test on 3 different resolutions.
- Testing. Check 15 edge values, typical user errors, correct formatting.
- Documentation and handover. Prepare a config description, setup and maintenance instructions. Hand over in a git repository.
What if the formula is complex?
Sometimes a formula contains many dependent parameters or requires iterative calculation. In such cases, we break it into sub-formulas, each tested separately. For example, for a loan calculator with differentiated payments, we extracted the calculation of each month into a separate function, simplifying debugging.
Timelines and what's included
| Calculator type | Timeline | What's included |
|---|---|---|
| Simple (3–5 fields, one result) | from 1 day | Config, UI, validation, basic animation |
| Medium (dependent fields, multiple results) | 2–3 days | Added dependencies, URL sharing, responsive |
| Complex (multiple calculators, CMS management) | 1–2 weeks | Admin panel, reports, PDF/Excel export, full documentation |
The work includes: git repository with source code, deployment instructions, 30-day warranty on bug fixes, post-launch support. We use a modern stack (React 18, TypeScript, Tailwind) and monitor Core Web Vitals (LCP < 2.5s, CLS < 0.1).
We will evaluate your project within 1 day. Order custom online calculator development today and get a reliable solution with quality guarantee. Contact us for a consultation and assessment of your requirements.







