Developing tabs and accordions using Vue.js for 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
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    811
  • 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
    745
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    976

Tab and Accordion Development on Vue.js for 1C-Bitrix

Tab components and accordions seem like a simple task until requirements arrive: preserve the active tab in the URL, lazily load tab content on first open, synchronize the state of multiple accordions on a page. Standard Bitrix templates implement tabs via jQuery .show()/.hide() — all content is rendered into the DOM at once, the URL is not updated, and deep-linking to a tab is impossible. Vue solves these problems without extra code.

Architecture of Vue Components for Bitrix

Tabs are built from a pair of components: TabGroup manages the active state, TabPanel renders the content of the active tab. The key decision is how to store the active tab:

  • ref() inside the component — when the URL is not needed
  • useRoute from Vue Router — for SPA pages with full routing
  • URLSearchParams via native history.pushState — for Bitrix pages without Vue Router, when deep-linking is required
// Saving the active tab in the URL without Vue Router
const activeTab = ref(new URLSearchParams(location.search).get('tab') || 'description');

watch(activeTab, (tab) => {
    const url = new URL(location.href);
    url.searchParams.set('tab', tab);
    history.replaceState({}, '', url);
});

This allows users to copy a link to a specific tab — critical for product pages with "Specifications", "Reviews", and "Delivery" tabs.

Accordions require smooth height animation, which is non-trivial in CSS: height: auto cannot be animated. The solution uses Vue's <Transition> with JavaScript hooks:

function onEnter(el) {
    el.style.height = '0';
    el.offsetHeight; // force reflow
    el.style.height = el.scrollHeight + 'px';
}
function onAfterEnter(el) {
    el.style.height = 'auto';
}
function onLeave(el) {
    el.style.height = el.scrollHeight + 'px';
    el.offsetHeight;
    el.style.height = '0';
}

This delivers native height animation without libraries, working correctly regardless of the content inside.

Integration with Bitrix Data

Tab content from infoblocks. A product page with tabs: "Description" — DETAIL_TEXT from b_iblock_element, "Specifications" — properties from b_iblock_element_property, "Reviews" — data from the reviews infoblock. In result_modifier.php an array is assembled:

$arResult['TABS'] = [
    ['id' => 'description', 'title' => 'Description', 'content' => $arResult['DETAIL_TEXT']],
    ['id' => 'props', 'title' => 'Specifications', 'props' => $arResult['DISPLAY_PROPERTIES']],
    ['id' => 'reviews', 'title' => 'Reviews', 'lazy' => true, 'endpoint' => '/api/reviews/?id=' . $arResult['ID']],
];

The "Reviews" tab is flagged with lazy: true — content is loaded via AJAX only on first open. The component's loading state shows a spinner during the request.

FAQ accordion from an infoblock. The bitrix:news.list component template outputs questions and answers. The Vue component mounts on the container, parsing the existing HTML via querySelectorAll (progressive enhancement) or receiving data via data-items JSON.

Case: Pricing Page Tabs with Dynamic Period Switching

A SaaS product on Bitrix, a pricing page with "Monthly" / "Annual" tabs. When switching tabs, prices on all plan cards must change without a page reload.

The jQuery solution required duplicating all the HTML with prices — one set for monthly prices, another for annual, then toggling visibility. With 6 plans — 12 HTML blocks, hard to maintain.

The Vue solution: a single PricingTabs component, data from window.BX_STATE.pricing:

const period = ref('monthly'); // 'monthly' | 'yearly'
const plans = computed(() =>
    rawPlans.value.map(plan => ({
        ...plan,
        price: period.value === 'yearly'
            ? Math.round(plan.yearlyPrice / 12)
            : plan.monthlyPrice,
        badge: period.value === 'yearly' ? `Save ${plan.yearlySaving}%` : null
    }))
);

Prices are stored in JSON and computed reactively when the period changes. Price-change animation — <Transition name="price-flip"> with CSS rotateX. Data in PHP is populated from the "Plans" infoblock with MONTHLY_PRICE and YEARLY_PRICE fields. Development — 2 days.

Accessibility and SEO

Tabs without ARIA is a common problem. Correct markup:

<div role="tablist">
  <button role="tab" :aria-selected="activeTab === 'desc'" :aria-controls="'panel-desc'">
    Description
  </button>
</div>
<div role="tabpanel" id="panel-desc" :hidden="activeTab !== 'desc'">
  <!-- content -->
</div>

The hidden attribute instead of v-show with display: none — screen readers correctly ignore hidden panels. Keyboard navigation between tabs — ArrowLeft/ArrowRight via onKeydown.

SEO aspect: search engines index tab content if it is present in the HTML. With v-if (lazy rendering) — the first tab is rendered server-side via the Bitrix template, the rest are loaded via AJAX. This is acceptable for Googlebot — it executes JavaScript, but with a delay. For SEO-critical content (product specifications) — always render in HTML, hide via hidden.

Scope of Work

Component Estimated timeline
Tabs with static content + URL persistence 1-2 days
Tabs with lazy-loaded content 2-3 days
Accordion with animation 1 day
Linked tabs + dynamic data (like the pricing example) 3-5 days

Timelines are determined by the number of data sources, animation requirements, and the complexity of Bitrix component integration.