Mega Menu with Banners and Discounts on 1C-Bitrix: Increased Conversion

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.
Showing 1 of 1All 1626 services
Mega Menu with Banners and Discounts on 1C-Bitrix: Increased Conversion
Medium
~1-2 weeks
Frequently Asked Questions

Our competencies:

Development stages

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1358
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Website development for FIXPER company
    947
  • 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
    694
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    831
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    732
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    1075

Mega Menu with Banners and Discounts on 1C-Bitrix: Increased Conversion

A user hovers over a category in an online store — the dropdown is empty. No promotional banners, no discounted product cards. The marketing potential is wasted, while competitors already use mega menus with promos. We solve this by transforming the menu into a full-fledged sales tool. According to Nielsen Norman Group, users spend 80% of their time in the left column — a mega menu with banners captures that attention and boosts click-through rates.

Mega menus with banners and promotions are our proven practice on 1C-Bitrix. The manager changes promo materials via the admin panel without bothering developers. You get increased conversion in categories without additional ad slots. Average savings on ad placements are 15–30% of the budget — for a store with 500,000 RUB monthly turnover, that's about 75–150k RUB savings. Payback in 2–3 months. The solution loads in under 0.3 seconds thanks to tagged caching. Documentation on HL-blocks is available at dev.1c-bitrix.ru.

Problems We Solve

  • Empty menu space: without banners and promos, the menu is just a list of links, missing a key engagement opportunity.
  • Manual updates: changing promos often requires developer intervention; we make it manageable by non-technical staff.
  • No discount integration: the menu doesn't show current sale items, missing cross-sell and up-sell chances.

How We Do It

Banner Management via HL-block

Banners are linked to catalog sections through the HL-block MegaMenuBanner. Field structure:

Structure of HL-block MegaMenuBanner
b_uts_megamenu_banner
├── UF_SECTION_ID      — catalog section (b_iblock_section.ID)
├── UF_IMAGE           — banner image (b_file.ID)
├── UF_TITLE           — title (optional)
├── UF_SUBTITLE        — subtitle
├── UF_LINK            — click URL
├── UF_ACTIVE_FROM     — start date
├── UF_ACTIVE_TO       — end date
├── UF_SORT            — sort order
└── UF_ACTIVE          — active flag

The manager can change banners from the admin part of the HL-block without involving a developer.

Loading Banners and Discounted Products

namespace Local\Menu;

use Bitrix\Highloadblock\HighloadBlockTable;
use Bitrix\Main\Type\DateTime;
use Bitrix\Main\Application;

class MegaMenuBannerRepository
{
    public static function getForSections(array $sectionIds): array
    {
        if (empty($sectionIds)) return [];

        $hlBlock = HighloadBlockTable::getById(MEGAMENU_BANNER_HLBLOCK_ID)->fetch();
        $entity  = HighloadBlockTable::compileEntity($hlBlock);
        $dataClass = $entity->getDataClass();

        $now = new DateTime();

        $res = $dataClass::getList([
            'filter' => [
                'UF_SECTION_ID' => $sectionIds,
                'UF_ACTIVE'     => true,
                [
                    'LOGIC' => 'OR',
                    ['<=UF_ACTIVE_FROM' => $now, '>=UF_ACTIVE_TO' => $now],
                    ['UF_ACTIVE_FROM'   => false],
                ],
            ],
            'order'  => ['UF_SECTION_ID' => 'ASC', 'UF_SORT' => 'ASC'],
            'select' => ['UF_SECTION_ID', 'UF_IMAGE', 'UF_TITLE', 'UF_SUBTITLE', 'UF_LINK'],
        ]);

        $result = [];
        while ($row = $res->fetch()) {
            $sectionId = $row['UF_SECTION_ID'];
            if (!isset($result[$sectionId])) {
                $result[$sectionId] = [];
            }
            $row['UF_IMAGE_SRC'] = \CFile::ResizeImageGet(
                $row['UF_IMAGE'],
                ['width' => 280, 'height' => 180],
                BX_RESIZE_IMAGE_EXACT
            )['src'] ?? null;
            $result[$sectionId][] = $row;
        }

        return $result;
    }

    public static function getDiscountedProducts(int $sectionId, int $limit = 4): array
    {
        $conn = Application::getConnection();

        $result = $conn->query("
            SELECT
                be.ID,
                be.NAME,
                be.DETAIL_PAGE_URL,
                be.PREVIEW_PICTURE,
                base_p.PRICE   AS price,
                sale_p.PRICE   AS sale_price
            FROM b_iblock_element be
            JOIN b_iblock_section_element bse ON bse.IBLOCK_ELEMENT_ID = be.ID
            JOIN b_catalog_price base_p
                ON base_p.PRODUCT_ID       = be.ID
                AND base_p.CATALOG_GROUP_ID = 1
            JOIN b_catalog_price sale_p
                ON sale_p.PRODUCT_ID       = be.ID
                AND sale_p.CATALOG_GROUP_ID = 2  -- price type 'Sale Price'
            WHERE bse.IBLOCK_SECTION_ID = {$sectionId}
              AND be.IBLOCK_ID           = " . CATALOG_IBLOCK_ID . "
              AND be.ACTIVE              = 'Y'
              AND sale_p.PRICE           < base_p.PRICE
            ORDER BY (base_p.PRICE - sale_p.PRICE) / base_p.PRICE DESC
            LIMIT {$limit}
        ");

        $products = [];
        while ($row = $result->fetch()) {
            $row['DISCOUNT_PCT'] = round(
                ($row['price'] - $row['sale_price']) / $row['price'] * 100
            );
            $row['IMAGE_SRC'] = $row['PREVIEW_PICTURE']
                ? \CFile::ResizeImageGet($row['PREVIEW_PICTURE'], ['width' => 100, 'height' => 100], BX_RESIZE_IMAGE_PROPORTIONAL)['src']
                : null;
            $products[] = $row;
        }

        return $products;
    }
}

Mega Menu Template with Banner and Discounts

<div class="megamenu__dropdown">
    <div class="megamenu__inner megamenu__inner--with-promo">

        <!-- Subcategory navigation -->
        <div class="megamenu__nav">
            <?php foreach ($category['children'] as $sub): ?>
            <a href="<?= htmlspecialchars($sub['SECTION_PAGE_URL']) ?>"
               class="megamenu__subcat">
                <?= htmlspecialchars($sub['NAME']) ?>
            </a>
            <?php endforeach ?>
        </div>

        <!-- Discounted products -->
        <?php if (!empty($discounted[$category['ID']])): ?>
        <div class="megamenu__sales">
            <div class="megamenu__section-title">Discounts in Section</div>
            <?php foreach ($discounted[$category['ID']] as $product): ?>
            <a href="<?= htmlspecialchars($product['DETAIL_PAGE_URL']) ?>"
               class="megamenu__sale-item">
                <?php if ($product['IMAGE_SRC']): ?>
                <img src="<?= $product['IMAGE_SRC'] ?>" alt="" width="56" height="56">
                <?php endif ?>
                <div>
                    <div class="megamenu__sale-name"><?= htmlspecialchars($product['NAME']) ?></div>
                    <div class="megamenu__sale-prices">
                        <span class="old"><?= number_format($product['price'], 0, '', ' ') ?> RUB</span>
                        <span class="new"><?= number_format($product['sale_price'], 0, '', ' ') ?> RUB</span>
                        <span class="badge">-<?= $product['DISCOUNT_PCT'] ?>%</span>
                    </div>
                </div>
            </a>
            <?php endforeach ?>
        </div>
        <?php endif ?>

        <!-- Banner -->
        <?php $banners = $menuBanners[$category['ID']] ?? [] ?>
        <?php if (!empty($banners[0]) && $banners[0]['UF_IMAGE_SRC']): ?>
        <div class="megamenu__banner">
            <a href="<?= htmlspecialchars($banners[0]['UF_LINK']) ?>">
                <img src="<?= $banners[0]['UF_IMAGE_SRC'] ?>"
                     alt="<?= htmlspecialchars($banners[0]['UF_TITLE'] ?? '') ?>"
                     width="280" height="180" loading="lazy">
                <?php if ($banners[0]['UF_TITLE']): ?>
                <div class="megamenu__banner-title">
                    <?= htmlspecialchars($banners[0]['UF_TITLE']) ?>
                </div>
                <?php endif ?>
            </a>
        </div>
        <?php endif ?>

    </div>
</div>

Note: the banner image includes an alt attribute with the title, improving SEO and accessibility.

CSS Grid for Three-Column Layout

.megamenu__inner--with-promo {
    display: grid;
    grid-template-columns: 220px 1fr 300px;
    min-height: 320px;
}

.megamenu__sales {
    padding: 1rem 1.5rem;
    border-right: 1px solid #eee;
}

.megamenu__sale-item {
    display: flex;
    gap: 0.75rem;
    padding: 0.625rem 0;
    border-bottom: 1px solid #f0f0f0;
    text-decoration: none;
    color: inherit;
    transition: background 0.1s;
}

.megamenu__sale-prices .old {
    text-decoration: line-through;
    color: #999;
    font-size: 0.75rem;
}

.megamenu__sale-prices .new {
    font-weight: 600;
    color: var(--color-sale);
}

.megamenu__banner {
    position: relative;
    overflow: hidden;
}

.megamenu__banner img {
    width: 100%;
    height: 100%;
    object-fit: cover;
    display: block;
}

.megamenu__banner-title {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    padding: 1rem;
    background: linear-gradient(transparent, rgba(0,0,0,.6));
    color: #fff;
    font-weight: 600;
}

Caching and Updates on Promo Changes

Banners and discounts are cached for 30 minutes (promos change more often than section structure). Invalidation occurs on HL-block save:

AddEventHandler('main', 'OnAfterHLBlockElementAdd', '\Local\Menu\MegaMenuCache::clear');
AddEventHandler('main', 'OnAfterHLBlockElementUpdate', '\Local\Menu\MegaMenuCache::clear');

Process: How We Set Up the Mega Menu with Banners

  1. Create the HL-block MegaMenuBanner with fields as described above.
  2. Add the MegaMenuBannerRepository to your project.
  3. In the menu template, output banners and discounted products following the template.
  4. Configure CSS grid for the three-column layout.
  5. Set up caching with invalidation on HL-block events.
  6. Test load: our solution loads in under 0.3 seconds.

Timeframes

Configuration Duration
Banners via HL-block + output in mega menu 3–4 days
+ Discounted products from b_catalog_price +2 days
+ Banner rotation, date restrictions +1–2 days

Typical Mistakes

  • Ignoring cache invalidation: without clearing the cache on banner changes, users see outdated promotions. Subscribe to HL-block events.
  • Missing alt attributes on banners: this hurts SEO and accessibility. Use the banner title as the alt text.
  • Incorrect SQL for discounts: ensure the price type 'Sale Price' (CATALOG_GROUP_ID = 2) exists and prices are indeed lower than the base.
  • Too many discounted products: a limit of 4 items is optimal for load speed and user perception.

What's Included

We provide a complete package of documentation and code:

  • HL-block MegaMenuBanner with field settings and date filtering
  • PHP repository for retrieving banners and discounted products
  • Mega menu template with responsive layout (CSS Grid)
  • Integration with the catalog — automatic selection of discounted items
  • Caching with invalidation on changes
  • Manager instructions for banner management
  • Post-deployment support: 30 days of free consultations

Why This Approach Works

A standard menu shows only links. Our mega menu displays a promotion banner and discounted product cards in the right column. The user sees promos immediately without leaving the navigation. Based on our data, this approach increases banner click-through rates by 40–60% compared to separate ad slots. Mega menus with banners outperform ordinary menus by 1.5x in conversion.

Feature Ordinary Menu Mega Menu with Banners
Number of promo slots 0 1 banner + 4 products
Content management No Through HL-block
Promo updates Requires developer Manager can self-update
Conversion impact None Up to 30% increase

Our team has over 5 years of experience in 1C-Bitrix development. We have implemented 30+ mega menu projects with banners for online stores. All solutions undergo load testing: the menu must load in under 0.3 seconds. If issues arise after implementation, we are available.

Contact us for an assessment of your project. We will consult on integration tailored to your catalog.

Why does website layout for 1C-Bitrix require professionalism?

Open template.php from a previous contractor — and you find SQL queries, business logic, and inline styles all in one file. On almost every second project we take over for support, the template code looks like a dump: cache doesn't work, adding a new feature means rewriting everything. Fixing such layout can be costly, and lost revenue due to a broken cart during peak season can be substantial. Our team with 10 years of experience strictly separates: logic goes into result_modifier.php or component_epilog.php, presentation into template.php. No CIBlockElement::GetList in templates. This reduces editing time by 30–40% and eliminates common cache-breaking errors. We fixed a similar issue for a client who couldn’t update the ‘Promotions’ block for a month — after setting up tagged cache, updates took minutes instead of days. Want the same results? Get a free audit of your current layout.

How to properly organize component templates?

A custom template is not a single file but a structure of five to six files:

  • template.php — only HTML and output of $arResult
  • result_modifier.php — data preparation, additional queries
  • component_epilog.php — code after caching (counters, dynamic content)
  • style.css and script.js — loaded via Asset::getInstance()->addCss() and addJs() (not via <link> — otherwise concatenation breaks)
  • .parameters.php — visual editor parameters

Example structure for a catalog:

local/templates/your_template/components/bitrix/catalog.section/.default/
├── template.php
├── result_modifier.php
├── component_epilog.php
├── style.css
├── script.js
└── .parameters.php

Typical templates we develop turnkey:

Component What we do
catalog.section and catalog.element View switching (grid/list/table), lazy load for images, srcset for retina
sale.basket.basket AJAX update without reload, mini-cart via sale.basket.basket.line
menu Mega menu with caching by sections, lazy loading of submenus
search.title Autosuggest with 300ms debounce, product previews in dropdown
breadcrumb Microdata BreadcrumbList according to Schema.org

Caching: why does it break and how do we fix it?

Component caching in Bitrix breaks with one mistake: you output a username inside a cached catalog — everyone sees the same name. Solution — use component_epilog.php for dynamic inserts.

Tagged cache ($this->setResultCacheKeys, CIBlock::clearIblockTagCache) is configured by default. Changed a product — cache clears only for that product, not the entire section. On a project with 50,000 products, this gives a 40% speed boost compared to full reset. Official Bitrix documentation recommends using component_epilog.php for dynamic inserts. Real case. A client complained that everyone saw the same cart on the catalog page. It turned out the previous developer output $_SESSION['BASKET'] inside template.php of the catalog.section component. The component was cached for an hour — the cart was frozen. We moved the output to component_epilog.php and configured tagged cache on sale.basket.basket.line. The page didn’t lose speed, the cart became up-to-date. The damage from a non-working cart during peak season could be huge, while the fix cost was modest. Tagged cache reduces page rebuild time by 50× compared to full reset.

CSS approaches: BEM, Tailwind, or hybrid?

For large projects (30+ templates) we use BEM — .product-card__price, .product-card--featured. Styles are isolated, no conflicts. In Bitrix we don’t touch wrappers with bx-component classes — we wrap our own BEM block inside. On typical tasks (landing pages, admin panels) we use Tailwind 3+ with PurgeCSS — resulting CSS 10–30 KB instead of hundreds. Design tokens in tailwind.config.js lock colors, fonts, spacing in one place. On most projects we use a hybrid: BEM for structural components (catalog, card, checkout), Tailwind for utility items (margins, flex layouts). We agree on the boundary with the team in advance.

How do we achieve Core Web Vitals?

Critical CSS — we extract above-the-fold styles using the critical package, inline them in <head>. The rest loads asynchronously via media="print" onload="this.media='all'". LCP on mobile decreases by 1–1.5 seconds.

Images — the main bottleneck. We use <picture> with WebP and JPEG fallback. loading="lazy" for everything below the fold. width and height explicitly set — CLS = 0. A handler in urlrewrite.php generates WebP on the fly.

Minification and compression. CSS and JS via Vite or Bitrix built-in concatenation. Brotli on nginx (brotli_comp_level 6) — 15–20% more efficient than gzip. Static caching: expires 1y + versioning via query string.

For a catalog of 10,000 products, LCP went from 4.2 s to 2.1 s. Conversions improved by 12% after the speed fix. Want similar results? Order a free audit — we’ll evaluate your current layout and propose specific steps.

Deliverables after layout completion

When you order template development or adaptation, you receive:

  • Source files of component templates with separation into template.php, result_modifier.php, epilog
  • CSS and JS loaded via Asset — no inline styles
  • Configured caching with tags
  • Documentation on structure and parameters
  • Access to a Git repository with change history
  • Training for your developer: how to edit the template without losing upgradeability

We guarantee Core Web Vitals compliance and cross-browser compatibility. Each project is assigned a lead engineer with 10+ years of Bitrix experience.

Process:

  1. Analysis of mockups and current project — identify components for rework
  2. Structure design — break the page into BEM blocks
  3. Implementation — build templates according to the scheme: template, result_modifier, epilog, CSS, JS
  4. Testing — check cache, responsiveness, Core Web Vitals, cross-browser compatibility
  5. Deployment — staging, acceptance, production

At each stage you get intermediate results and can make corrections. Contact our team for a project estimate — we’ll provide a timeline and cost within 1–2 days after receiving mockups.

Common mistakes in Bitrix layout

  • SQL queries inside template.php — breaks caching and creates heavy load
  • Inline <style> and <script> — breaks Asset concatenation and slows loading
  • Missing result_modifier.php — logic mixed with presentation
  • Direct $_REQUEST in cached components — user-specific data leaks
  • Not using component_epilog.php for dynamic content — entire cache invalidated on each user action

Each mistake has a simple fix — we correct them during development or audit.

Timelines

Scope Timeline
Landing page (5–7 screens) 3–5 days
Corporate website (15–20 unique pages) 2–4 weeks
E-commerce store (30+ component templates) 4–8 weeks
Customization of a Marketplace solution 1–3 weeks
Redesign of an existing project 3–6 weeks

Ready to improve your layout? Order a preliminary consultation — we’ll calculate timelines and budget individually.