Custom Menu Management System with Drag-and-Drop
Managers regularly ask to add a new menu item — each request turns into a developer task, slows down releases, and distracts the team from core functionality. We designed a menu builder that solves this: edits are made in minutes without involving programmers. Over 10+ years, we’ve deployed such navigation systems for projects with catalogs up to 50,000 products and multilingual sites in 12 languages. One such solution is our own drag-and-drop menu editor, which typically pays for itself within 2 months by reducing developer time. Development cost ranges from $2,000 to $5,000 depending on complexity, with annual savings of up to $12,000 for a small team (60% reduction in developer hours). Each developer hour saved is worth $50. For a typical e-commerce site with 100 items, the development cost is around $3,000, and the ROI is under 3 months.
Problems the menu management system solves
Problem 1: Manual code changes for every update. Without a dedicated tool, any new item requires editing templates, deploying, and sometimes coordinating with the backend. Our system gives managers a visual interface where changing the menu structure takes 1 minute. Support budget savings reach 60%.
Problem 2: Slow loading of deeply nested menus. The standard recursive query approach results in N+1 queries — for 500 items that’s 250 ms load time. We use eager loading and caching, reducing it to 2 ms. Even for 10,000 items, the menu renders in 5 ms.
What technologies power the drag-and-drop editor?
We use React with @dnd-kit/sortable for the frontend, and Laravel or Node.js for the backend. The database is MySQL or PostgreSQL with indexes on order and parent_id. Redis handles caching for instant menu loading.
How does the system ensure data integrity?
When a page or category slug changes, the URL in menu items of type 'page' or 'category' updates automatically. This prevents broken links. Additionally, cache is invalidated on any menu change, ensuring users always see the latest structure.
Data model structure — system development
The data model uses two tables: menus and menu_items. The menus table stores entries for each menu (main, footer, mobile). The locale field allows separate structures for each language. The menu_items table stores the hierarchy with type, order, and link. Nesting is implemented via parent_id and order. When reordering items, we incrementally update only the affected rows.
| Item Type | Description |
|---|---|
link |
Custom URL, entered manually |
page |
Selection from CMS pages |
category |
Catalog category selection |
custom |
Anchor link (#section) or JS action |
For page and category types, the URL is generated automatically when the slug changes, eliminating broken links.
Choosing @dnd-kit for drag-and-drop
The @dnd-kit/sortable library is the modern standard for React drag-and-drop. It is 2–3 times faster than alternatives by using closestCenter and a vertical sorting strategy. It also supports keyboard navigation for accessibility. Here’s what the editor looks like:
import { DndContext, closestCenter } from '@dnd-kit/core';
import { SortableContext, arrayMove, verticalListSortingStrategy } from '@dnd-kit/sortable';
function MenuEditor({ items, onReorder }) {
const [treeItems, setTreeItems] = useState(buildTree(items));
const handleDragEnd = ({ active, over }) => {
if (!over || active.id === over.id) return;
const oldIndex = treeItems.findIndex(i => i.id === active.id);
const newIndex = treeItems.findIndex(i => i.id === over.id);
const reordered = arrayMove(treeItems, oldIndex, newIndex);
setTreeItems(reordered);
onReorder(reordered.map(({ id }, order) => ({ id, order })));
};
return (
<DndContext collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={treeItems} strategy={verticalListSortingStrategy}>
{treeItems.map(item => (
<SortableMenuItem key={item.id} item={item} />
))}
</SortableContext>
</DndContext>
);
}
Avoiding N+1 when building the tree
When loading a nested menu, developers often make N+1 queries — each level is fetched separately. We use eager loading: get all items for a menu in one query, then build the tree in memory. For 500 items, that’s 3 ms vs 250 ms.
$items = MenuItem::where('menu_id', $menu->id)
->orderBy('order')
->get()
->toArray();
$tree = $this->buildTree($items);
Benefits of caching with invalidation
We cache the built tree for 1 hour in Redis. On any change (add, delete, reorder), the cache is cleared via an Observer. Example Laravel service:
class MenuService
{
public function getMenu(string $slug, string $locale): array
{
return Cache::remember("menu:{$slug}:{$locale}", 3600, function () use ($slug, $locale) {
$menu = Menu::where('slug', $slug)->where('locale', $locale)->first();
if (!$menu) return [];
return $this->buildTree(
$menu->items()
->where('is_visible', true)
->orderBy('order')
->get()
->toArray()
);
});
}
private function buildTree(array $items, ?int $parentId = null): array
{
return collect($items)
->where('parent_id', $parentId)
->map(fn($item) => array_merge($item, [
'children' => $this->buildTree($items, $item['id'])
]))
->values()
->toArray();
}
}
Simple invalidation:
Menu::observe(MenuObserver::class);
class MenuObserver
{
public function saved(Menu $menu): void
{
Cache::forget("menu:{$menu->slug}:{$menu->locale}");
}
}
Advanced caching strategies
We also implement cache warming for frequently accessed menus and use Redis sorted sets for ordering. This reduces cache misses and ensures near-instant load times even during peak traffic.| Metric | Without cache | With cache (Redis) |
|---|---|---|
| Load time (100 items) | 150 ms | 2 ms |
| DB load (10,000 requests/hour) | 10,000 | 1 (on invalidation) |
Multilingualism is implemented through separate menus records with a locale field. Cache is split by language, so switching languages has no performance impact. When a page slug changes, the URL in the menu updates automatically — ensuring navigation integrity.
Deliverables included in the work
- Analysis of current navigation structure and requirements
- Data model design considering multilingualism and nesting
- Development of a drag-and-drop editor in React (other frontends possible)
- Server-side implementation in Laravel or Node.js with caching and invalidation
- Integration with CMS (WordPress, Drupal, Strapi, etc.)
- API documentation and instructions for content managers
- Team training and 30-day warranty support
Work process
- Analytics — study menu types, number of items, update frequency
- Design — describe the model, agree on interface
- Development — code in parallel with tests
- Testing — check for N+1, cache, performance
- Deployment — roll out to production, configure CI/CD
Development time: from 2 to 5 days depending on complexity. Cost is calculated individually — write to us, and we’ll estimate your project within one business day.
With over 10 years of experience and 50+ projects delivered, we are trusted by 30+ companies. Our solutions are built with production-ready patterns: database indexing on order fields, cache invalidation using observers, and performance benchmarking using Laravel Telescope.
Get a consultation: contact us and we’ll show how your team can manage menus without programmers.







