Developing CRM Cards on Vue.js for Bitrix24
The standard deal, contact, or lead card in Bitrix24 covers 80% of needs. Problems begin with the remaining 20%: displaying data from an external system directly in the card, adding a button with non-trivial behavior, showing related objects as a table, or making fields dependent on one another. Bitrix24 provides an embedding mechanism — placement — through which a Vue application mounts directly inside the CRM card.
The Placement Mechanism in CRM
BX24.placement.getInterface() returns information about the current context — where exactly the card is open and which object is displayed:
window.BX24.init(() => {
BX24.placement.getInterface((result) => {
// result: { ID, ENTITY_TYPE_NAME, ENTITY_TYPE_ID }
// e.g.: { ID: 123, ENTITY_TYPE_NAME: 'deal', ENTITY_TYPE_ID: 2 }
initApp(result)
})
})
Placement types for CRM:
-
CRM_DEAL_DETAIL_TAB— tab in the deal card -
CRM_CONTACT_DETAIL_TAB— tab in the contact card -
CRM_LEAD_DETAIL_TAB— tab in the lead card -
CRM_DEAL_DETAIL_ACTIVITY— activities block
Vue Card Architecture
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
window.BX24.init(async () => {
const placement = await getPlacement()
const auth = BX24.getAuth()
const app = createApp(App)
app.use(createPinia())
app.provide('entityId', placement.ID)
app.provide('entityType', placement.ENTITY_TYPE_NAME)
app.provide('auth', auth)
app.mount('#app')
})
function getPlacement() {
return new Promise(resolve => BX24.placement.getInterface(resolve))
}
Pinia for card state management is the optimal choice: supports DevTools, TypeScript, works without this.
Loading Entity Data
// stores/dealStore.js
import { defineStore } from 'pinia'
import { inject } from 'vue'
export const useDealStore = defineStore('deal', {
state: () => ({
deal: null,
relatedContacts: [],
externalData: null,
loading: false,
}),
actions: {
async loadDeal(id) {
this.loading = true
const [deal, contacts] = await Promise.all([
bx24Call('crm.deal.get', { id }),
bx24Call('crm.deal.contact.items.get', { id }),
])
this.deal = deal
this.relatedContacts = contacts
this.loading = false
},
async loadExternalData(dealId) {
// Data from an external system via a custom API
const res = await fetch(`/api/deals/${dealId}/external`)
this.externalData = await res.json()
}
}
})
Updating Card Fields
Updating a CRM object via REST:
async function updateDeal(id, fields) {
return new Promise((resolve, reject) => {
BX24.callMethod('crm.deal.update', {
id,
fields,
}, (result) => {
if (result.error()) reject(result.error())
else resolve(result.data())
})
})
}
For multiple fields simultaneously — use a batch request. Do not update fields one by one in a loop — this creates a request queue and slows down the interface.
Custom Buttons and Actions
Adding a button to the card toolbar via BX24.placement.bindEvent:
BX24.placement.bindEvent('onAppOptionsSave', () => {
// React to saving application settings
})
// Action button is registered via callMethod
BX24.callMethod('placement.bind', {
PLACEMENT: 'CRM_DEAL_DETAIL_TAB',
HANDLER: 'https://my-app.example.com/',
TITLE: 'My Tab',
DESCRIPTION: 'CRM card extension',
})
Inside the Vue application itself, buttons are regular components emitting events and calling REST methods.
Displaying Data from External Systems
A typical use case — showing a customer's order history from 1C or an ERP in the deal card:
<template>
<div class="external-orders">
<div v-if="store.loading" class="loader">Loading...</div>
<table v-else>
<thead>
<tr>
<th>Order Number</th>
<th>Date</th>
<th>Amount</th>
<th>Status</th>
</tr>
</thead>
<tbody>
<tr v-for="order in store.externalData?.orders" :key="order.id">
<td>{{ order.number }}</td>
<td>{{ formatDate(order.date) }}</td>
<td>{{ formatMoney(order.total) }}</td>
<td>
<span :class="statusClass(order.status)">
{{ order.statusLabel }}
</span>
</td>
</tr>
</tbody>
</table>
</div>
</template>
The server side is a proxy endpoint that authenticates with 1C, retrieves the data, and returns JSON. Bitrix24 does not access 1C directly from the frontend.
Reactive Updates on Changes
If a user changed a field in the standard card and switched to your tab — data needs to be reloaded:
// Check relevance when the tab gains focus
document.addEventListener('visibilitychange', () => {
if (!document.hidden) store.loadDeal(entityId)
})
Alternatively — subscribe to events via BX24.placement.bindEvent('onAppOptionsSave', callback).
Styling to Match the Bitrix24 Interface
Use CSS variables and neutral components. Bitrix24 has its own design system, and a custom tab should look organic within it. Minimal shadows, thin borders, sans-serif fonts. Headless UI or custom components are preferable over heavy UI libraries with bold themes.
Timeline
A simple tab displaying REST data with an action button — 2–4 business days. A full-featured tab with external system data, editing, and synchronization — 1–3 weeks depending on the number of entities and the complexity of the logic.







