Development of a Multi-Step Form for 1C-Bitrix
A long form on a single page kills conversion—confirmed by our A/B tests across 40+ projects. Users see 10+ fields and close the tab within 5 seconds. The solution is to split the form into steps of 2–4 fields each. Each screen feels simple, progress is visible, and conversion jumps by 30–50%. We develop custom components for Bitrix and Bitrix24 with client-side navigation, per-step validation, and final submission to CRM. The foundation is the standard component 2.0 mechanism and infoblocks v2.0. The server side creates leads via CCrmLead, supports business processes and REST API. For long forms, we implement progress persistence in localStorage to prevent data loss on accidental closure. This approach has delivered 2–3x conversion growth across 20 projects on average.
| Characteristic | Single-screen form | Multi-step form |
|---|---|---|
| Conversion | 2–5% | 8–15% |
| Bounce rate | 30–50% | 10–20% |
| Fill time | 3–5 min | 1–2 min (subjective) |
| Usability | low | high |
Why a multi-step form increases conversion?
The progress bar and step breakdown psychologically lower the entry threshold. Users see "only 3 steps" and are ready to begin. Each completed step gives a sense of progress. In practice, we see a 40% increase in completion rates compared to single-screen forms. Additionally, per-step validation eliminates end-of-form errors. Multi-step forms also yield higher-quality leads because users don't tire and enter data more carefully. This is especially important for B2B requests with technical fields.
What technical issues arise during implementation?
The main challenges are synchronizing state between steps, handling server-side errors after final submission, and correct validation with input masks. If a user enters a phone with a mask and the server validation is stricter, the lead may not be created. We solve this by unifying validation on client and server. Another issue is data loss on browser close; we implement localStorage persistence with a 30-minute timeout. Our certified specialists have solved these issues on 50+ projects.
How is the multi-step form component structured?
The form holds all state client-side as a JS object and sends data in a single request after the last step. The server receives the complete dataset.
Step configuration is done via component parameters (PHP array) or through the admin interface using infoblocks.
// /local/components/local/multistep.form/class.php
namespace Local\Form;
class MultistepFormComponent extends \CBitrixComponent
{
public function executeComponent(): void
{
// Configure steps
$this->arResult['STEPS'] = $this->arParams['STEPS'] ?? $this->getDefaultSteps();
$this->arResult['FORM_ACTION'] = '/local/ajax/multistep_form.php';
$this->arResult['SESSID'] = bitrix_sessid();
$this->includeComponentTemplate();
}
private function getDefaultSteps(): array
{
return [
[
'id' => 'step_personal',
'title' => 'Your details',
'fields' => [
['name' => 'name', 'label' => 'Name', 'type' => 'text', 'required' => true],
['name' => 'phone', 'label' => 'Phone', 'type' => 'tel', 'required' => true],
['name' => 'email', 'label' => 'Email', 'type' => 'email', 'required' => false],
],
],
[
'id' => 'step_details',
'title' => 'Request details',
'fields' => [
['name' => 'service', 'label' => 'Service', 'type' => 'select', 'options' => ['Consultation', 'Audit', 'Development']],
['name' => 'budget', 'label' => 'Budget', 'type' => 'radio', 'options' => ['up to 100k', '100-500k', 'over 500k']],
],
],
[
'id' => 'step_message',
'title' => 'Comment',
'fields' => [
['name' => 'message', 'label' => 'Describe the task', 'type' => 'textarea', 'required' => false],
],
],
];
}
}
Client-side logic
class MultistepForm {
constructor(formEl) {
this.form = formEl;
this.steps = Array.from(formEl.querySelectorAll('.form-step'));
this.current = 0;
this.formData = {};
this.bindEvents();
this.showStep(0);
}
bindEvents() {
this.form.querySelectorAll('.btn-next').forEach(btn =>
btn.addEventListener('click', () => this.tryNext())
);
this.form.querySelectorAll('.btn-prev').forEach(btn =>
btn.addEventListener('click', () => this.prev())
);
this.form.addEventListener('submit', e => {
e.preventDefault();
this.submit();
});
}
tryNext() {
if (!this.validateCurrentStep()) return;
this.collectCurrentStepData();
if (this.current < this.steps.length - 1) {
this.showStep(this.current + 1);
}
}
prev() {
if (this.current > 0) {
this.showStep(this.current - 1);
}
}
showStep(index) {
this.steps.forEach((step, i) => {
step.classList.toggle('active', i === index);
});
this.current = index;
this.updateProgress();
}
validateCurrentStep(): boolean {
const stepEl = this.steps[this.current];
let valid = true;
stepEl.querySelectorAll('[required]').forEach(field => {
const error = stepEl.querySelector(`[data-error-for="${field.name}"]`);
if (!field.value.trim()) {
valid = false;
field.classList.add('error');
if (error) error.textContent = 'This field is required';
} else {
field.classList.remove('error');
if (error) error.textContent = '';
}
});
// Phone validation
const phoneField = stepEl.querySelector('[name="phone"]');
if (phoneField && phoneField.value) {
const phoneRegex = /^(\+7|7|8)?[\s-]?\(?\d{3}\)?[\s-]?\d{3}[\s-]?\d{2}[\s-]?\d{2}$/;
if (!phoneRegex.test(phoneField.value.replace(/\s/g, ''))) {
valid = false;
phoneField.classList.add('error');
}
}
return valid;
}
collectCurrentStepData() {
const stepEl = this.steps[this.current];
stepEl.querySelectorAll('input, select, textarea').forEach(field => {
if (field.type === 'radio' || field.type === 'checkbox') {
if (field.checked) this.formData[field.name] = field.value;
} else {
this.formData[field.name] = field.value;
}
});
}
updateProgress() {
const percent = ((this.current) / (this.steps.length - 1)) * 100;
const bar = this.form.querySelector('.progress-bar-fill');
if (bar) bar.style.width = Math.min(100, percent) + '%';
const label = this.form.querySelector('.progress-label');
if (label) label.textContent = `Step ${this.current + 1} of ${this.steps.length}`;
}
async submit() {
this.collectCurrentStepData();
const submitBtn = this.form.querySelector('[type="submit"]');
submitBtn.disabled = true;
submitBtn.textContent = 'Sending...';
try {
const response = await fetch(this.form.dataset.action, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
...this.formData,
sessid: this.form.dataset.sessid,
}),
});
const result = await response.json();
if (result.success) {
this.showSuccessScreen();
} else {
this.showError(result.error || 'Submission error');
submitBtn.disabled = false;
submitBtn.textContent = 'Send';
}
} catch (e) {
this.showError('Connection error. Please try again later.');
submitBtn.disabled = false;
}
}
showSuccessScreen() {
this.form.innerHTML = `
<div class="form-success">
<h3>Thank you! We'll contact you within an hour.</h3>
</div>
`;
}
showError(message) {
let errEl = this.form.querySelector('.form-global-error');
if (!errEl) {
errEl = document.createElement('div');
errEl.className = 'form-global-error';
this.steps[this.current].prepend(errEl);
}
errEl.textContent = message;
}
}
document.querySelectorAll('.multistep-form').forEach(el => new MultistepForm(el));
Server-side handler
// /local/ajax/multistep_form.php
\Bitrix\Main\Loader::includeModule('crm');
$data = json_decode(file_get_contents('php://input'), true);
$sessid = $data['sessid'] ?? '';
if (!check_bitrix_sessid($sessid)) {
http_response_code(403);
echo json_encode(['error' => 'Forbidden']);
exit;
}
$name = htmlspecialchars(trim($data['name'] ?? ''));
$phone = htmlspecialchars(trim($data['phone'] ?? ''));
$email = htmlspecialchars(trim($data['email'] ?? ''));
$service = htmlspecialchars($data['service'] ?? '');
$budget = htmlspecialchars($data['budget'] ?? '');
$message = htmlspecialchars($data['message'] ?? '');
if (empty($name) || empty($phone)) {
echo json_encode(['error' => 'Required fields are missing']);
exit;
}
$comments = implode("\n", array_filter([
$service ? "Service: {$service}" : '',
$budget ? "Budget: {$budget}" : '',
$message ? "Comment: {$message}" : '',
]));
$lead = new \CCrmLead(false);
$leadId = $lead->Add([
'TITLE' => 'Website request — ' . $name,
'NAME' => $name,
'PHONE' => [['VALUE' => $phone, 'VALUE_TYPE' => 'WORK']],
'EMAIL' => [['VALUE' => $email, 'VALUE_TYPE' => 'WORK']],
'SOURCE_ID' => 'WEB',
'COMMENTS' => $comments,
]);
echo json_encode(['success' => (bool)$leadId]);
Progress persistence: how to avoid data loss?
For long forms (5+ steps), we add localStorage persistence — on accidental close, data is restored at the same step. Timeout is 30 minutes; beyond that, data is considered stale.
// After each step
localStorage.setItem('form_progress', JSON.stringify({
step: this.current,
data: this.formData,
savedAt: Date.now(),
}));
// On init — restore if not older than 30 minutes
const saved = JSON.parse(localStorage.getItem('form_progress') || 'null');
if (saved && Date.now() - saved.savedAt < 1800000) {
this.formData = saved.data;
this.showStep(saved.step);
}
Step-by-step setup guide
- Create a new component in
local/components/company/multistep.form. - Define the steps array with fields (as in the example above).
- Implement client-side JavaScript with validation.
- Write the server handler to create leads.
- Include the component on a page and configure parameters.
Typical implementation mistakes
- Missing CSRF protection — always use
bitrix_sessid(). - Data type mismatch between client and server.
- Ignoring mobile version — check step responsiveness.
Development stages
- Analysis — identify required fields and step logic.
- Design — prepare mockups and component configuration.
- Development — create PHP component and JS file.
- Testing — A/B tests, validation checks, error handling.
- Deployment — deploy to live site and integrate with CRM.
What's included in the work
- API documentation for the component and setup instructions.
- Access to the source code repository.
- Training for managers on form usage (filling, editing steps).
- Warranty support for 2 months after deployment.
Timelines and cost
| Option | Scope | Estimated timeline |
|---|---|---|
| Basic multi-step form | 2–3 steps, validation, lead to CRM | 2–4 days |
| With custom fields | Select, radio, file, dynamic fields | 4–7 days |
| Full component | Configurable steps, A/B testing, analytics, persistence | 8–12 days |
Cost is calculated individually based on number of steps, validation complexity, and integration needs. Contact us for a preliminary estimate for your project. Order multi-step form development to boost your site's conversion. Get a consultation — we'll analyze your current form and propose the optimal solution.







