Megamenu development 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
    1175
  • 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

Developing a Mega Menu in 1C-Bitrix

The standard bitrix:menu component renders a flat list of links. A large online store requires something different: multi-column navigation across sections, quick access to the second and third hierarchy levels without extra clicks, support for thousands of categories without performance degradation. A mega menu is a separate component built on top of the infoblock section tree.

Data Source: Section Tree

The Bitrix section structure is stored in b_iblock_section. A mega menu needs two to three levels of nesting. The standard CIBlockSection::GetList method with INCLUDE_SUBSECTIONS = 'N' does not return a tree — only a flat list. We build the tree manually:

// /local/lib/Menu/MegaMenuBuilder.php
namespace Local\Menu;

class MegaMenuBuilder
{
    public function build(int $iblockId, int $depth = 3): array
    {
        $cache = new \CPHPCache();
        $cacheId = 'megamenu_' . $iblockId . '_' . LANGUAGE_ID;

        if ($cache->InitCache(3600, $cacheId, '/megamenu/')) {
            return $cache->GetVars()['menu'];
        }

        $raw = $this->fetchSections($iblockId, $depth);
        $tree = $this->buildTree($raw);

        $cache->StartDataCache();
        $cache->EndDataCache(['menu' => $tree]);

        return $tree;
    }

    private function fetchSections(int $iblockId, int $depth): array
    {
        $result = [];
        $res = \CIBlockSection::GetList(
            ['LEFT_MARGIN' => 'ASC'],
            [
                'IBLOCK_ID' => $iblockId,
                'ACTIVE'    => 'Y',
                'DEPTH_LEVEL' => [$depth], // up to the required depth
                'GLOBAL_ACTIVE' => 'Y',
            ],
            false,
            ['ID', 'NAME', 'CODE', 'DEPTH_LEVEL', 'IBLOCK_SECTION_ID',
             'SECTION_PAGE_URL', 'PICTURE', 'UF_MENU_ICON', 'LEFT_MARGIN', 'RIGHT_MARGIN']
        );

        while ($section = $res->GetNext()) {
            $result[$section['ID']] = $section;
        }

        return $result;
    }

    private function buildTree(array $flat, int $parentId = 0): array
    {
        $tree = [];
        foreach ($flat as $section) {
            if ((int)$section['IBLOCK_SECTION_ID'] === $parentId) {
                $section['children'] = $this->buildTree($flat, (int)$section['ID']);
                $tree[] = $section;
            }
        }
        return $tree;
    }
}

Mega Menu Component

The component is registered at /local/components/local/megamenu/. The template receives the section tree and renders HTML:

// /local/components/local/megamenu/class.php
class LocalMegaMenuComponent extends \CBitrixComponent
{
    public function onPrepareComponentParams($params): array
    {
        $params['IBLOCK_ID'] = (int)($params['IBLOCK_ID'] ?? CATALOG_IBLOCK_ID);
        $params['DEPTH']     = (int)($params['DEPTH'] ?? 3);
        return $params;
    }

    public function executeComponent(): void
    {
        $builder = new \Local\Menu\MegaMenuBuilder();
        $this->arResult['MENU'] = $builder->build(
            $this->arParams['IBLOCK_ID'],
            $this->arParams['DEPTH']
        );

        $this->setFrameMode(false); // disable Bitrix edit mode
        $this->includeComponentTemplate();
    }
}

Mega Menu Template

// /local/components/local/megamenu/templates/.default/template.php
/** @var array $arResult */
?>
<nav class="megamenu" aria-label="Catalogue navigation">
    <ul class="megamenu__list">
        <?php foreach ($arResult['MENU'] as $category): ?>
        <li class="megamenu__item" data-id="<?= $category['ID'] ?>">
            <a href="<?= htmlspecialchars($category['SECTION_PAGE_URL']) ?>"
               class="megamenu__link">
                <?php if ($category['UF_MENU_ICON']): ?>
                    <img src="<?= \CFile::GetPath($category['UF_MENU_ICON']) ?>"
                         alt="" class="megamenu__icon" loading="lazy">
                <?php endif ?>
                <span><?= htmlspecialchars($category['NAME']) ?></span>
            </a>

            <?php if (!empty($category['children'])): ?>
            <div class="megamenu__dropdown">
                <div class="megamenu__columns">
                    <?php foreach (array_chunk($category['children'], 8) as $col): ?>
                    <div class="megamenu__col">
                        <?php foreach ($col as $sub): ?>
                        <a href="<?= htmlspecialchars($sub['SECTION_PAGE_URL']) ?>"
                           class="megamenu__sublink">
                            <?= htmlspecialchars($sub['NAME']) ?>
                        </a>
                        <?php if (!empty($sub['children'])): ?>
                        <ul class="megamenu__tertiary">
                            <?php foreach (array_slice($sub['children'], 0, 5) as $third): ?>
                            <li>
                                <a href="<?= htmlspecialchars($third['SECTION_PAGE_URL']) ?>">
                                    <?= htmlspecialchars($third['NAME']) ?>
                                </a>
                            </li>
                            <?php endforeach ?>
                        </ul>
                        <?php endif ?>
                        <?php endforeach ?>
                    </div>
                    <?php endforeach ?>
                </div>
            </div>
            <?php endif ?>
        </li>
        <?php endforeach ?>
    </ul>
</nav>

CSS: Opening Without JavaScript

The mega menu functions via CSS :hover as a baseline — a courtesy to users with slow JavaScript:

.megamenu__dropdown {
    position: absolute;
    top: 100%;
    left: 0;
    width: 100%;
    background: #fff;
    border-top: 2px solid var(--color-primary);
    box-shadow: 0 8px 24px rgba(0,0,0,.12);
    display: none;
    z-index: 1000;
}

.megamenu__item:hover .megamenu__dropdown,
.megamenu__item:focus-within .megamenu__dropdown {
    display: block;
}

.megamenu__columns {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 1.5rem;
    padding: 1.5rem;
    max-width: 1200px;
    margin: 0 auto;
}

JavaScript: Close Delay and Accessibility

// 200ms close delay — prevents flickering when moving the mouse
document.querySelectorAll('.megamenu__item').forEach(item => {
    let timer;
    item.addEventListener('mouseenter', () => {
        clearTimeout(timer);
        item.querySelector('.megamenu__dropdown')?.style.setProperty('display', 'block');
    });
    item.addEventListener('mouseleave', () => {
        timer = setTimeout(() => {
            item.querySelector('.megamenu__dropdown')?.style.setProperty('display', 'none');
        }, 200);
    });
});

// Keyboard navigation
document.querySelectorAll('.megamenu__link').forEach(link => {
    link.addEventListener('keydown', e => {
        if (e.key === 'Enter' || e.key === ' ') {
            const dropdown = link.nextElementSibling;
            if (dropdown) {
                e.preventDefault();
                dropdown.style.display = dropdown.style.display === 'block' ? 'none' : 'block';
            }
        }
    });
});

Caching and Invalidation

The section tree is cached for 3600 seconds. When any catalogue section is modified, the OnAfterIBlockSectionUpdate event handler clears the cache:

AddEventHandler('iblock', 'OnAfterIBlockSectionUpdate', function($fields) {
    $cache = new \CPHPCache();
    $cache->CleanDir('/megamenu/');
});

Performance

The section tree is built with a single query to b_iblock_section, and the result is cached. The mega menu generates no additional SQL queries on the page. With 500 sections across three nesting levels, the query takes 3–8 ms and the PHP tree construction takes another 2–5 ms. A full render of the cached mega menu takes less than 1 ms.

Implementation Timeline

Configuration Timeline
Basic mega menu (2 levels, hover) 3–4 days
With third level, caching, invalidation 5–7 days
Responsive version with mobile drawer +3–4 days
With images, banners, promotions +2–3 days