Development of Multi-Step Calculator on 1C-Bitrix
A multi-step calculator is a questionnaire broken down into sequential screens. Instead of one long form, users see 2–3 questions per step, a progress bar, and smooth transitions forward. Psychologically it's simpler: each step is small, abandoning halfway through is harder than dropping an entire long form. According to A/B test data, multi-step calculators convert on average 30–50% better than comparable single-page forms.
Architecture of a Multi-Step Scenario
Calculator steps are not simply pages. They form a graph: some steps are skipped depending on previous answers. For example, if a user selects "individual" on step 2, step 3 with company details is skipped.
Storing step configuration — JSON or HL-block CalculatorSteps:
{
"steps": [
{
"id": "service_type",
"title": "What interests you?",
"type": "single_choice",
"options": [
{ "value": "moving", "label": "Moving" },
{ "value": "storage", "label": "Storage" },
{ "value": "both", "label": "Moving + storage" }
]
},
{
"id": "area",
"title": "Space area",
"type": "range_slider",
"min": 20, "max": 500, "step": 10, "default": 60,
"unit": "m²",
"condition": { "field": "service_type", "operator": "in", "value": ["moving", "both"] }
},
{
"id": "contacts",
"title": "Where to send the estimate?",
"type": "contact_form",
"fields": ["name", "phone", "email"]
}
]
}
Frontend: Managing Steps with JavaScript
class MultiStepCalculator {
constructor(config) {
this.steps = config.steps;
this.answers = {};
this.history = []; // for "Back" button
this.currentStepIndex = 0;
}
getCurrentStep() {
return this.steps[this.currentStepIndex];
}
next(answer) {
const step = this.getCurrentStep();
this.answers[step.id] = answer;
this.history.push(this.currentStepIndex);
// Find next step that is not skipped
let nextIndex = this.currentStepIndex + 1;
while (nextIndex < this.steps.length) {
if (this.checkCondition(this.steps[nextIndex].condition)) {
break;
}
nextIndex++;
}
if (nextIndex >= this.steps.length) {
this.submit(); // all steps completed
} else {
this.currentStepIndex = nextIndex;
this.render();
}
this.updateProgress();
}
back() {
if (this.history.length === 0) return;
this.currentStepIndex = this.history.pop();
this.render();
this.updateProgress();
}
checkCondition(condition) {
if (!condition) return true; // no condition — step always shown
const value = this.answers[condition.field];
switch (condition.operator) {
case 'eq': return value === condition.value;
case 'in': return condition.value.includes(value);
case 'gt': return parseFloat(value) > parseFloat(condition.value);
case 'not': return value !== condition.value;
default: return true;
}
}
updateProgress() {
const visible = this.steps.filter((_, i) =>
i <= this.currentStepIndex || this.checkCondition(this.steps[i]?.condition)
);
const pct = Math.round((this.currentStepIndex / (this.steps.length - 1)) * 100);
document.getElementById('progress-bar').style.width = pct + '%';
document.getElementById('progress-text').textContent = `Step ${this.currentStepIndex + 1} of ${visible.length}`;
}
async submit() {
const resp = await fetch('/ajax/calculator/multistep/submit/', {
method: 'POST',
body: new URLSearchParams({
answers: JSON.stringify(this.answers),
sessid: BX.bitrix_sessid(),
}),
});
const result = await resp.json();
this.showResult(result);
}
}
Server-Side Calculation: From Answers to Result
namespace MyProject\Controllers;
use Bitrix\Main\Engine\Controller;
class MultistepCalculatorController extends Controller
{
public function submitAction(string $answersJson): array
{
$answers = json_decode($answersJson, true);
if (!$answers) {
$this->addError(new \Bitrix\Main\Error('Invalid data'));
return [];
}
// Validation of required fields
$required = ['service_type', 'area'];
foreach ($required as $field) {
if (!isset($answers[$field])) {
$this->addError(new \Bitrix\Main\Error("Field not filled: {$field}"));
return [];
}
}
// Calculation through service class
$calcResult = \MyProject\Services\MovingCalculator::calculate($answers);
// Create lead in CRM
$leadId = \MyProject\Services\CrmService::createLeadFromCalculator(
$answers['name'] ?? 'Not specified',
$answers['phone'] ?? '',
$answers,
$calcResult
);
return [
'result' => $calcResult,
'lead_id' => $leadId,
];
}
}
Saving Progress: Preventing Data Loss
If a user accidentally closes the tab — data is saved to localStorage:
// Auto-save on every answer
saveProgress() {
localStorage.setItem('calc_progress', JSON.stringify({
answers: this.answers,
stepIndex: this.currentStepIndex,
savedAt: Date.now(),
}));
}
// Restore on load
restoreProgress() {
const saved = localStorage.getItem('calc_progress');
if (!saved) return;
const data = JSON.parse(saved);
const ageMs = Date.now() - data.savedAt;
if (ageMs > 24 * 60 * 60 * 1000) { // older than 24 hours — don't restore
localStorage.removeItem('calc_progress');
return;
}
this.answers = data.answers;
this.currentStepIndex = data.stepIndex;
this.render();
}
Analytics: Funnel by Steps
Every transition between steps is an analytics event:
next(answer) {
// ... transition logic
// Record step in Yandex Metrica
ym(COUNTER_ID, 'reachGoal', 'calc_step_complete', {
step: this.getCurrentStep().id,
value: JSON.stringify(answer),
});
}
A funnel by steps shows where users drop off. If 70% abandon on step 3 — it's either too complex or inappropriate.
Timeline
| Task | Duration |
|---|---|
| Multi-step calculator (3–5 steps, linear scenario, contact form) | 1–2 weeks |
| Calculator with branching scenario, conditional steps, AJAX calculation | 3–5 weeks |
| Full configurator (10+ steps, complex rules, visualization, history) | 6–10 weeks |
A multi-step calculator is the most effective format for complex services. It reduces cognitive load, increases engagement, and gives marketers data on each decision-making stage — exactly what prevents users from reaching the lead form.







