Developing a quiz form 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
    1189
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    813
  • 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
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Quiz Form Development on 1C-Bitrix

A quiz form is a questionnaire that, upon completion, offers the user a result (a personalized offer, calculation, or recommendation) in exchange for their contact details. Quiz conversion rates are often 2–5 times higher than regular forms: the user has already invested time in answering and wants to see the result. On 1C-Bitrix, a quiz is implemented as a custom component with state stored via session or localStorage.

Quiz Architecture

A quiz is a sequence of steps. Each step is a question with one or more answer options. Data is structured in an infoblock or HL-block:

Infoblock quizzes — quizzes (one element = one quiz):

Property Code Type
Result title RESULT_TITLE String
CTA text on the form CTA_TEXT String
Lead recipient NOTIFY_EMAIL String
CRM lead tag CRM_TAG String
Cover image COVER_IMAGE File

HL-block b_hl_quiz_questions — questions:

class QuizQuestionTable extends \Bitrix\Main\ORM\Data\DataManager
{
    public static function getTableName(): string { return 'b_hl_quiz_questions'; }

    public static function getMap(): array
    {
        return [
            new IntegerField('ID', ['primary' => true, 'autocomplete' => true]),
            new IntegerField('QUIZ_ID'),
            new IntegerField('SORT'),
            new StringField('TEXT'),              // Question text
            new StringField('TYPE'),              // single | multiple | image_choice
            new TextField('OPTIONS_JSON'),        // JSON: [{id, text, image?, weight?}]
            new BooleanField('IS_REQUIRED', ['values' => [false, true]]),
            new StringField('HINT'),              // Question hint (optional)
        ];
    }
}

PHP Quiz Component

// /local/components/local/quiz/class.php
namespace Local\Quiz;

class QuizComponent extends \CBitrixComponent
{
    public function executeComponent(): void
    {
        $quizId = (int)$this->arParams['QUIZ_ID'];

        // Load quiz and questions
        $quiz      = \CIBlockElement::GetByID($quizId)->GetNext();
        $questions = QuizQuestionTable::getList([
            'filter' => ['QUIZ_ID' => $quizId],
            'order'  => ['SORT' => 'ASC'],
        ])->fetchAll();

        foreach ($questions as &$q) {
            $q['OPTIONS'] = json_decode($q['OPTIONS_JSON'], true) ?? [];
        }

        $this->arResult = [
            'QUIZ'      => $quiz,
            'QUESTIONS' => $questions,
            'TOTAL'     => count($questions),
        ];

        $this->includeComponentTemplate();
    }
}

Frontend: State and Navigation

The quiz is a SPA-like UI on a single page. State is stored in a JS object:

const quizState = {
    currentStep: 0,       // Current question (0-indexed)
    answers: {},          // {questionId: [selectedOptionIds]}
    contactData: null,    // Data from the final form
    startTime: Date.now(),
};

function goToStep(step) {
    document.querySelectorAll('.quiz-step').forEach(el => el.classList.remove('active'));
    document.querySelector(`.quiz-step[data-step="${step}"]`)?.classList.add('active');
    quizState.currentStep = step;
    updateProgressBar();
}

function selectOption(questionId, optionId, isMultiple) {
    if (!quizState.answers[questionId]) {
        quizState.answers[questionId] = [];
    }

    if (isMultiple) {
        const idx = quizState.answers[questionId].indexOf(optionId);
        if (idx === -1) {
            quizState.answers[questionId].push(optionId);
        } else {
            quizState.answers[questionId].splice(idx, 1);
        }
    } else {
        quizState.answers[questionId] = [optionId];
        // Automatically move to the next question
        setTimeout(() => nextStep(), 300);
    }
}

function updateProgressBar() {
    const total    = parseInt(document.getElementById('quiz-total').value);
    const progress = ((quizState.currentStep) / total) * 100;
    document.getElementById('quiz-progress-fill').style.width = progress + '%';
}

Final Form and Submission

After the last question, a contact details form is shown. Submission is done via AJAX:

async function submitQuiz(formData) {
    const payload = {
        quiz_id:    document.getElementById('quiz-id').value,
        answers:    quizState.answers,
        name:       formData.get('name'),
        phone:      formData.get('phone'),
        email:      formData.get('email'),
        time_spent: Math.round((Date.now() - quizState.startTime) / 1000),
        sessid:     BX.bitrix_sessid(),
    };

    const response = await fetch('/local/ajax/quiz_submit.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/json'},
        body: JSON.stringify(payload),
    });

    const result = await response.json();
    if (result.success) {
        goToStep('result'); // Show result page
    }
}

Server-Side Handler

// /local/ajax/quiz_submit.php

$data   = json_decode(file_get_contents('php://input'), true);
$quizId = (int)($data['quiz_id'] ?? 0);
$name   = htmlspecialchars($data['name'] ?? '');
$phone  = htmlspecialchars($data['phone'] ?? '');

// Save answers to the HL-block
QuizResponseTable::add([
    'QUIZ_ID'    => $quizId,
    'USER_IP'    => $_SERVER['REMOTE_ADDR'],
    'ANSWERS'    => json_encode($data['answers']),
    'TIME_SPENT' => (int)($data['time_spent'] ?? 0),
    'NAME'       => $name,
    'PHONE'      => $phone,
    'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);

// Create a lead in CRM
if (\Bitrix\Main\Loader::includeModule('crm')) {
    $quiz = \CIBlockElement::GetByID($quizId)->GetNext();
    $lead = new \CCrmLead(false);
    $lead->Add([
        'TITLE'              => 'Quiz: ' . $quiz['NAME'] . ' — ' . $name,
        'NAME'               => $name,
        'PHONE'              => [['VALUE' => $phone, 'VALUE_TYPE' => 'WORK']],
        'SOURCE_ID'          => 'WEB',
        'SOURCE_DESCRIPTION' => 'Quiz: ' . $quiz['NAME'],
        'COMMENTS'           => 'Answers: ' . json_encode($data['answers'], JSON_UNESCAPED_UNICODE),
    ]);
}

echo json_encode(['success' => true]);

Quiz Analytics

The HL-block b_hl_quiz_stats stores aggregated statistics:

  • How many users started the quiz.
  • At which step they dropped off (allows improving weak questions).
  • Conversion: started → completed → left contact.
// Record quiz start
QuizStatsTable::add([
    'QUIZ_ID'    => $quizId,
    'SESSION_ID' => session_id(),
    'STEP'       => 0,
    'EVENT'      => 'start',
    'CREATED_AT' => new \Bitrix\Main\Type\DateTime(),
]);

Development Timeline

Option Scope Timeline
Single quiz (static) Component, questions in code, lead in CRM 3–5 days
Quiz with management Infoblock/HL-block, question management via admin panel 5–8 days
Full constructor Multiple quizzes, branching, analytics, A/B 12–18 days