Modal Window and Pop-up Development on Vue.js for 1C-Bitrix
Standard Bitrix components use jQuery dialogs from BX.PopupWindow — they work until you need complex logic inside: multi-step forms, reactive validation, asynchronous content loading. Trying to extend BX.PopupWindow with custom HTML quickly turns into a fight with its internal rendering. Vue.js solves this cleanly — a modal window component gets reactive state, props, emit events, and full control over the DOM.
Architecture of a Vue.js Modal Window in Bitrix
Vue integration in Bitrix is built through a mount point in the component template. Instead of initializing the application globally, we mount an isolated instance on a specific element:
// local/templates/main/components/project/modal-form/template.php
<div id="vue-modal-root"
data-form-id="<?= $arResult['FORM_ID'] ?>"
data-endpoint="<?= $arResult['AJAX_URL'] ?>">
</div>
<script>
BX.ready(function() {
const { createApp } = Vue;
const app = createApp(ModalApp, {
formId: document.getElementById('vue-modal-root').dataset.formId,
endpoint: document.getElementById('vue-modal-root').dataset.ajaxEndpoint
});
app.mount('#vue-modal-root');
});
</script>
Data from Bitrix is passed via data-* attributes or through the global JS object window.BX_STATE, which is assembled in result_modifier.php. Direct PHP interpolation in JS code is avoided — it breaks component caching.
Vue modal component structure:
-
ModalWrapper.vue— wrapper with overlay, managesz-index, scroll locking (document.body.style.overflow), andEscapekey handling -
ModalContent.vue— slot for content, enter/leave animations via<Transition> -
ModalTrigger.vue— trigger button, emits the open event viaprovide/injector a Pinia store
For managing the state of multiple modals on a page, we use useModalStore on Pinia — a stack of open modals, so closing the top one doesn't close the one below.
Typical Scenarios and Their Implementation
Contact form with validation. The most common case. Fields are validated reactively via VeeValidate or manually via computed. After submission — AJAX to the Bitrix handler via bitrix:form.result.new, the response is handled in the component. No page reload.
Modal cart. The user adds a product; the modal shows the current cart state. Data is retrieved via the Bitrix REST API: /api/v1/sale/basket/ or a custom AJAX controller based on Bitrix\Main\Engine\Controller. Reactive quantity update without a page reload.
Image gallery / lightbox. A Vue component receives an array of images via a data-images attribute or preloaded JSON. Swipe gestures via vue-use/useSwipe, keyboard navigation via onKeydown.
Multi-step forms. Steps as separate Vue components, state stored in the parent setup(). A progress bar reacts to the current step. Data from each step accumulates and is submitted in a single request at the final step.
Case: Lead Form Modal on a Promo Page
A product promo site with several CTA buttons on the page, each opening the same form but with a different utm_source and a pre-filled "How did you hear about us?" field.
The standard BX.PopupWindow solution would require JS DOM manipulation on every open. We solved it differently: one mount point in </body>, a Vue component LeadModal receiving props via eventBus. Buttons on the page dispatch a custom event:
document.querySelectorAll('[data-modal-trigger]').forEach(btn => {
btn.addEventListener('click', () => {
window.dispatchEvent(new CustomEvent('open-lead-modal', {
detail: { source: btn.dataset.source, productId: btn.dataset.productId }
}));
});
});
The Vue component listens for the event via onMounted:
window.addEventListener('open-lead-modal', (e) => {
formData.source = e.detail.source;
formData.productId = e.detail.productId;
isOpen.value = true;
});
The form submits data to the Bitrix crm module via REST API (crm.lead.add), the status is written to b_user_field via CUserTypeManager. Development took 3 days instead of a week for the jQuery version.
Performance and SEO
Modal windows render only when opened — we use v-if, not v-show, so the DOM doesn't bloat. For heavy components (video player, map) — defineAsyncComponent:
const HeavyMap = defineAsyncComponent(() => import('./HeavyMap.vue'));
The chunk loads only when the user opens the modal. Bitrix uses AssetManager to include the Vue bundle only on the pages that need it:
\Bitrix\Main\Page\Asset::getInstance()->addJs('/local/dist/modal-bundle.js');
Modals don't affect SEO — search engines don't index their content as primary. Links to promotions or products inside modals are duplicated in <noscript> or in hidden <a> tags for crawlers if the content matters for search.
Development Stages
- Analysis — inventory of existing pop-ups on the project, defining interaction scenarios, agreeing on states (open/closed/loading/error/success)
- Component prototype — base animation, scroll locking, close on overlay and Escape, focus trap for accessibility
- Data integration — connecting to the REST API or a custom Bitrix AJAX controller, error handling
-
Build and deployment — Vite build to
local/dist/, connected viaAssetManageronly on relevant pages
| Task complexity | Estimated timeline |
|---|---|
| One standard modal with a form | 1-2 days |
| A system of 3-5 related modals | 3-5 days |
| Multi-step form with validation and CRM integration | 5-10 days |
The final timeline depends on the number of states, the complexity of Bitrix integration, and animation requirements.







