Developing a multi-step calculator on 1C-Bitrix

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1175
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

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.