Developing a mega menu with product images in 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
    747
  • 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 with Product Images in 1C-Bitrix

A plain text list of subcategories works for a utilitarian store. For fashion, electronics, or furniture, a category is sold by an image: a coffee machine in the "Coffee Machines" section, a jacket in the "Outerwear" section. A mega menu with product previews or section images increases click-through rates and reduces navigation errors.

Infoblock Section Images

Infoblock sections in b_iblock_section have PICTURE and DETAIL_PICTURE fields. Additionally, the custom field UF_MENU_IMAGE can hold an image specifically adapted for the mega menu (different crop, different dimensions).

// When building the section tree, include image fields
$res = \CIBlockSection::GetList(
    ['LEFT_MARGIN' => 'ASC'],
    ['IBLOCK_ID' => $iblockId, 'ACTIVE' => 'Y', 'DEPTH_LEVEL' => [1, 2]],
    false,
    ['ID', 'NAME', 'CODE', 'SECTION_PAGE_URL', 'DEPTH_LEVEL',
     'IBLOCK_SECTION_ID', 'PICTURE', 'UF_MENU_IMAGE']
);

while ($s = $res->GetNext()) {
    // Priority: dedicated menu image > standard section image
    $imageId = $s['UF_MENU_IMAGE'] ?: $s['PICTURE'];
    $s['MENU_IMAGE_SRC'] = $imageId ? \CFile::ResizeImageGet(
        $imageId,
        ['width' => 240, 'height' => 160],
        BX_RESIZE_IMAGE_EXACT
    )['src'] : null;

    $result[$s['ID']] = $s;
}

Popular Product Images per Section

An alternative is to show 3–4 popular product previews in the mega menu rather than a section image. This is more dynamic and informative.

namespace Local\Menu;

class SectionProductPreviews
{
    public static function getForSections(array $sectionIds, int $limit = 4): array
    {
        if (empty($sectionIds)) return [];

        $cache = new \CPHPCache();
        $cacheId = 'menu_previews_' . md5(implode(',', $sectionIds));

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

        $result = [];
        foreach ($sectionIds as $sectionId) {
            $res = \CIBlockElement::GetList(
                ['RAND' => 'ASC'], // random, not always the same
                [
                    'IBLOCK_ID'         => CATALOG_IBLOCK_ID,
                    'SECTION_ID'        => $sectionId,
                    'ACTIVE'            => 'Y',
                    'PREVIEW_PICTURE'   => ['>', 0], // only products with an image
                ],
                false,
                ['nPageSize' => $limit, 'iNumPage' => 1],
                ['ID', 'NAME', 'PREVIEW_PICTURE', 'DETAIL_PAGE_URL']
            );

            $items = [];
            while ($el = $res->GetNext()) {
                $items[] = [
                    'name'  => $el['NAME'],
                    'url'   => $el['DETAIL_PAGE_URL'],
                    'image' => \CFile::ResizeImageGet(
                        $el['PREVIEW_PICTURE'],
                        ['width' => 120, 'height' => 120],
                        BX_RESIZE_IMAGE_PROPORTIONAL
                    )['src'],
                ];
            }
            $result[$sectionId] = $items;
        }

        $cache->StartDataCache();
        $cache->EndDataCache(['previews' => $result]);

        return $result;
    }
}

Mega Menu Template with Images

// In the template for each top-level category
foreach ($arResult['MENU'] as $category):
    $previews = $productPreviews[$category['ID']] ?? [];
?>
<div class="megamenu__dropdown megamenu__dropdown--with-images">
    <div class="megamenu__inner">

        <!-- Subcategory columns on the left -->
        <div class="megamenu__nav">
            <?php foreach ($category['children'] as $sub): ?>
            <a href="<?= htmlspecialchars($sub['SECTION_PAGE_URL']) ?>"
               class="megamenu__subcat"
               data-section="<?= $sub['ID'] ?>">
                <?php if ($sub['MENU_IMAGE_SRC']): ?>
                <img src="<?= $sub['MENU_IMAGE_SRC'] ?>"
                     alt="<?= htmlspecialchars($sub['NAME']) ?>"
                     width="60" height="40" loading="lazy">
                <?php endif ?>
                <span><?= htmlspecialchars($sub['NAME']) ?></span>
            </a>
            <?php endforeach ?>
        </div>

        <!-- Product previews on the right -->
        <?php if (!empty($previews)): ?>
        <div class="megamenu__products">
            <div class="megamenu__products-label">Popular products</div>
            <div class="megamenu__products-grid">
                <?php foreach ($previews as $product): ?>
                <a href="<?= htmlspecialchars($product['url']) ?>"
                   class="megamenu__product-card">
                    <img src="<?= htmlspecialchars($product['image']) ?>"
                         alt="<?= htmlspecialchars($product['name']) ?>"
                         width="120" height="120" loading="lazy">
                    <span class="megamenu__product-name">
                        <?= htmlspecialchars($product['name']) ?>
                    </span>
                </a>
                <?php endforeach ?>
            </div>
        </div>
        <?php endif ?>

    </div>
</div>
<?php endforeach ?>

CSS Layout with Images

.megamenu__dropdown--with-images .megamenu__inner {
    display: grid;
    grid-template-columns: 260px 1fr;
    gap: 0;
    min-height: 300px;
}

.megamenu__nav {
    border-right: 1px solid #eee;
    padding: 1rem;
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
}

.megamenu__subcat {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    padding: 0.5rem 0.75rem;
    border-radius: 4px;
    transition: background 0.15s;
    text-decoration: none;
    color: inherit;
}

.megamenu__subcat:hover,
.megamenu__subcat.is-active {
    background: #f5f7fa;
}

.megamenu__products-grid {
    display: grid;
    grid-template-columns: repeat(4, 1fr);
    gap: 1rem;
    padding: 1rem 1.5rem;
}

.megamenu__product-card {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    text-decoration: none;
    color: inherit;
    font-size: 0.8125rem;
}

.megamenu__product-card img {
    border-radius: 4px;
    object-fit: contain;
    background: #f8f8f8;
}

Dynamic Preview Loading

On hover over a subcategory, the products for that subcategory are loaded via AJAX — no need to pre-load everything:

document.querySelectorAll('.megamenu__subcat').forEach(link => {
    link.addEventListener('mouseenter', async () => {
        const sectionId = link.dataset.section;
        if (link.dataset.loaded) return;

        const data = await fetch(`/local/ajax/menu-products.php?section=${sectionId}`)
            .then(r => r.json());

        const grid = link.closest('.megamenu__dropdown').querySelector('.megamenu__products-grid');
        grid.innerHTML = data.products.map(p =>
            `<a href="${p.url}" class="megamenu__product-card">
                <img src="${p.image}" alt="${p.name}" width="120" height="120" loading="lazy">
                <span>${p.name}</span>
            </a>`
        ).join('');

        link.dataset.loaded = '1';
    });
});

Performance and Caching

Loading images for all products in all sections on the first page load is excessive. The strategy:

  • Section images (UF_MENU_IMAGE) — loaded together with the tree, cached for 1 hour
  • Product previews — lazy-loaded on hover via AJAX, 30-minute cache per section
  • Images with loading="lazy" — the browser does not load hidden panels

Implementation Timeline

Configuration Timeline
Mega menu with section images 4–5 days
+ popular product previews, AJAX loading +2–3 days
+ animations, accessibility, mobile version +2–3 days