Developing a Multi-Level Menu Module for 1C-Bitrix
The standard bitrix:menu component generates menus from .menu.php files or from the site structure. For simple sites this is sufficient. Problems begin when a marketer wants to manage a mega-menu with pictures, banners and columns through an administrative interface without code changes, when different menu structures need to be A/B tested, or when different menu items need to be shown to authorized and anonymous users. A multi-level menu module provides flexible management through an interface with caching and full mega-menu support.
Data Model
Module vendor.megamenu:
-
b_vendor_menu_config— menu configurations: id, code, name, site_id, lang, is_active -
b_vendor_menu_item— menu items: id, menu_id, parent_id, sort, title, url, url_type (absolute/relative/component), target, icon_id, image_id, css_class, visibility (all/authorized/unauthorized), condition (JSON), is_active -
b_vendor_menu_column— mega-menu columns for second-level items: id, item_id, title, sort, items (JSON — links without separate records) -
b_vendor_menu_banner— banners in mega-menu: id, item_id, image_id, url, title, sort
Tree of Menu Items
Items are stored in adjacency list (parent_id). To build the tree, iterative traversal is used to avoid recursion in PHP:
class MenuTreeBuilder
{
public function build(int $menuId): array
{
$items = MenuItemTable::getList([
'filter' => ['MENU_ID' => $menuId, 'IS_ACTIVE' => 'Y'],
'order' => ['PARENT_ID' => 'ASC', 'SORT' => 'ASC'],
])->fetchAll();
$tree = [];
$index = [];
foreach ($items as $item) {
$item['children'] = [];
$index[$item['ID']] = &$item;
}
foreach ($index as &$item) {
if ($item['PARENT_ID']) {
$index[$item['PARENT_ID']]['children'][] = &$item;
} else {
$tree[] = &$item;
}
}
return $tree;
}
}
The tree is cached entirely with the tag menu_{code}_{lang}. Invalidation occurs when any item or banner of this menu changes.
Item Visibility
Menu item visibility conditions:
-
visibility = 'authorized'— item is hidden from guests -
visibility = 'unauthorized'— item is hidden from logged-in users -
condition(JSON) — arbitrary conditions: user groups, region, request parameters
class VisibilityChecker
{
public function isVisible(array $item): bool
{
global $USER;
if ($item['VISIBILITY'] === 'authorized' && !$USER->IsAuthorized()) return false;
if ($item['VISIBILITY'] === 'unauthorized' && $USER->IsAuthorized()) return false;
$condition = $item['CONDITION'] ?? [];
if (!empty($condition['user_groups'])) {
$userGroups = $USER->GetUserGroupArray();
if (!array_intersect($condition['user_groups'], $userGroups)) return false;
}
return true;
}
}
Visibility conditions are applied after retrieving the cached tree — the cache stores the complete tree, filtering occurs in memory.
Mega-Menu with Columns and Banners
For top-level items, a mega-menu dropdown with columns can be configured:
[Catalog]
├─ Column 1: Electronics
│ ├─ Smartphones
│ ├─ Laptops
│ └─ Tablets
├─ Column 2: Home Appliances
│ ├─ Refrigerators
│ └─ Washing Machines
└─ Banner: [Weekly Sales → /sale/]
Columns are stored in b_vendor_menu_column, banners in b_vendor_menu_banner. The component generates the HTML structure for the mega-menu, styling is done through CSS Grid.
Drag-and-drop Interface
The administrative interface is built on Sortable.js (or similar). Items are dragged to the desired position and parent element. When dragging, an AJAX request updates sort and parent_id in a single transaction.
Active Menu Item
The active menu item is determined by the current URL:
// Exact URL match or path prefix match
function isActive(array $item, string $currentUrl): bool
{
if ($item['URL'] === $currentUrl) return true;
if ($item['URL_TYPE'] === 'section' && str_starts_with($currentUrl, $item['URL'])) return true;
return false;
}
The active item and all its ancestors receive the CSS class active.
Development Timeline
| Stage | Duration |
|---|---|
| ORM-tables, tree model | 1 day |
| Building tree, caching | 1 day |
| Item visibility conditions | 1 day |
| Mega-menu: columns, banners | 2 days |
| Drag-and-drop interface | 2 days |
| Site components (HTML + CSS) | 2 days |
| Testing | 1 day |
Total: 10 working days. For mobile menu (offcanvas, hamburger) — additional 1 day.







