Reactive product comparison component on Vue.js for Bitrix
Typical scenario: a user adds a product to comparison, the page reloads, the counter changes. After three clicks, they leave for a marketplace. The standard 1C-Bitrix comparison components (bitrix:catalog.compare.buy and bitrix:catalog.compare.result) worked fine years ago, but today UX expectations have changed radically. We offer to replace it with a modern Vue.js-based solution: adding to comparison is instant, the header counter updates without reload, and the table can hide identical characteristics. We develop the component turnkey, with adaptive layout and support.
Why replace the standard comparison with Vue.js?
The standard component uses PHP session to store the list — meaning a reload on every action. The Vue.js version works entirely on the frontend: state is kept in a Pinia store, synced with localStorage for guests and with the server for authorized users. This gives speed, responsiveness, and cross-session persistence. As a result, the time to add a product drops from 1–2 seconds to 50 ms, and cart conversion increases by 15–25%.
How the component works
Storing the comparison list
Standard Bitrix stores comparison in PHP session. We use localStorage with server sync for authorized users — so the list persists between sessions and devices.
// stores/compareStore.ts (Pinia)
export const useCompareStore = defineStore('compare', {
state: () => ({
items: [] as number[], // product IDs
}),
actions: {
async add(productId: number) {
if (this.items.includes(productId)) return
if (this.items.length >= 4) {
alert('You can compare up to 4 products at a time')
return
}
this.items.push(productId)
localStorage.setItem('compare_items', JSON.stringify(this.items))
// Sync with server for authorized users
if (isLoggedIn()) {
await fetch('/local/api/compare/add/', {
method: 'POST',
body: JSON.stringify({ product_id: productId }),
})
}
},
remove(productId: number) {
this.items = this.items.filter(id => id !== productId)
localStorage.setItem('compare_items', JSON.stringify(this.items))
},
isInCompare: (state) => (productId: number) => state.items.includes(productId),
},
getters: {
count: (state) => state.items.length,
},
})
On app initialization, the list is loaded from localStorage (for guests) or from the API (for authorized users, with merge across devices).
«Compare» button on the product card
<!-- CompareButton.vue -->
<template>
<button
:class="['compare-btn', { 'compare-btn--active': isAdded }]"
@click="toggle"
:title="isAdded ? 'Remove from comparison' : 'Add to comparison'"
>
<IconScale :filled="isAdded" />
<span>{{ isAdded ? 'In comparison' : 'Compare' }}</span>
</button>
</template>
<script setup lang="ts">
const props = defineProps<{ productId: number }>()
const store = useCompareStore()
const isAdded = computed(() => store.isInCompare(props.productId))
function toggle() {
isAdded.value ? store.remove(props.productId) : store.add(props.productId)
}
</script>
The button is mounted on each product card via a data-attribute.
Header counter
<!-- CompareCounter.vue (mounted in header) -->
<template>
<a href="/compare/" class="header-compare">
<IconScale />
<span class="badge" v-if="count > 0">{{ count }}</span>
</a>
</template>
<script setup lang="ts">
const store = useCompareStore()
const count = computed(() => store.count)
</script>
The counter reactively updates via Pinia — no global EventBus.
Comparison table with diff filter
The hardest part is the table. Data is loaded from the server by the list of IDs, then a difference map is built on the frontend. The «Show only differences» button hides rows with identical values.
<!-- CompareTable.vue (simplified) -->
<template>
<table class="compare-table">
<thead>
<tr>
<th>Characteristic</th>
<th v-for="p in products" :key="p.id">
<ProductCard :product="p" @remove="store.remove(p.id)" />
</th>
</tr>
</thead>
<tbody>
<tr
v-for="spec in visibleSpecs"
:key="spec"
:class="{ 'row--different': diffMap[spec] }"
>
<td class="spec-name">{{ specLabels[spec] }}</td>
<td v-for="p in products" :key="p.id">{{ p.specs[spec] ?? '—' }}</td>
</tr>
</tbody>
</table>
</template>
<script setup lang="ts">
const showOnlyDiff = ref(false)
const visibleSpecs = computed(() =>
showOnlyDiff.value
? Object.keys(diffMap.value).filter(k => diffMap.value[k])
: Object.keys(diffMap.value)
)
</script>
On mobile, the first column is fixed using CSS Grid with position: sticky.
How to integrate the component into the template?
Integration consists of three steps:
- Place mounting points in PHP templates:
data-compare-btnfor buttons on cards,data-compare-counterfor the counter in the header,data-compare-tablefor the comparison page. - Include the built Vue.js bundle via
RegisterModulein the template epilogue. - Set up an API controller to fetch product data by a list of IDs (properties with
COMPARE = 'Y').
Case study: 20% conversion increase after implementation
One of our clients — an online store with a catalog of ~5,000 products — faced low conversion on the comparison page (only 2% cart transitions). The standard component was slow: every action caused a reload. After implementing the Vue.js component, selection speed dropped by 70%, and the share of users who added a product to the cart from the comparison page grew to 22%. The key feature was the «Show only differences» button — customers found differences faster and made a decision.
Approach comparison: standard vs Vue.js
| Parameter | Standard component | Vue.js component |
|---|---|---|
| Product addition time | ~1-2 sec (with reload) | ~50 ms (no reload) |
| List persistence | Only PHP session | localStorage + server sync |
| Mobile adaptive | Limited | CSS Grid with sticky column |
| Difference filter | No | Yes |
| Cart conversion | ~2% | ~22% (case data) |
What's included
We deliver a ready-to-use component that includes:
- Source code in Vue.js 3 + Pinia + TypeScript.
- PHP controller for comparison API (optional).
- Integration instructions for the 1C-Bitrix template.
- Access to the repository with change history.
- Consultation support for 2 weeks after delivery.
Process and timeline
| Stage | Duration | Result |
|---|---|---|
| Analysis and design | 1–2 days | Technical specification, integration layout |
| Component development | 5–10 days | Working prototype |
| Integration and testing | 2–3 days | Full functionality on staging |
| Deployment and documentation | 1 day | Code and instructions handover |
Estimated timeline: 1 to 3 weeks depending on complexity. Contact us — we'll evaluate your project for free.
Our experience
We have been doing Bitrix development for over 5 years and have completed more than 50 projects, including custom components for online stores. We know all the pitfalls of integrating Vue.js with 1C-Bitrix and guarantee quality. Get a consultation — we'll discuss the details of your store.
Technical details (for developers)
Stack: Vue.js 3, Pinia, TypeScript, Vite. Build via Webpack or Vite, attached as a separate entry point in the template. The component does not conflict with other Vue applications on the page.
Infoblock properties participating in comparison are marked with the COMPARE = 'Y' flag — this is a standard Bitrix mechanism that requires no additional code.
Typical mistakes and their solutions
- Forgot about sync for authorized users. Without it, the list is lost when browser cache is cleared.
- Didn't account for the product limit. More than 4–5 products overload the interface — better to limit.
- Made a table without a fixed column on mobile. The user doesn't see characteristic names when scrolling horizontally.
All these issues are resolved at the development stage — contact us for a consultation.
Conclusion
Product comparison is an important tool for customers choosing among similar items. A properly implemented comparison helps make a decision on the site rather than leaving for an aggregator. Our component is fast, modern, and adaptive. Want to improve your store's UX? Contact us — we'll discuss the details and prepare a commercial proposal.







