A sophisticated JavaScript solution for creating dynamic dropdown and mega menus for Ghost CMS. This module provides flexible menu configuration, responsive design, accessibility features, and extensive customization options.
Current Version: 2.0.0
Release Date: July 9, 2026
License: MIT License
Author: Themeix (https://themeix.com)
Repository: https://github.com/themeix/ghost-dynamic-dropdown-mega-menu
- Include the JavaScript and CSS files in your Ghost theme:
<!-- In your default.hbs -->
<link rel="stylesheet" href="{{asset 'css/themeix-menu.css'}}">
<script src="{{asset 'js/themeix-menu.js'}}"></script>- Initialize the menu system:
// Initialize with default settings
ThemeixMenu.init();// Initialize with custom configuration
ThemeixMenu.init({
targetElement: '.gh-navigation-menu',
mobileBreakpoint: 768,
animationDuration: 300,
closeOnClickOutside: true,
closeOnEscape: true,
keyboardNavigation: true,
mouseDelay: 150,
childPrefix: '-',
debug: false
});const defaultConfig = {
targetElement: '.gh-navigation-menu', // CSS selector for navigation element
mobileBreakpoint: 768, // Pixel width for mobile switch
animationDuration: 300, // Animation duration in milliseconds
closeOnClickOutside: true, // Close menus when clicking outside
closeOnEscape: true, // Close menus on Escape key press
keyboardNavigation: true, // Enable keyboard navigation
mouseDelay: 150, // Delay before hover activation (ms)
childPrefix: '-', // Prefix for nested menu items
jsonConfigPath: null, // Path to external JSON config
preferCodeInjection: true, // Prefer Code Injection config over JSON file
defaultMenuSettings: { // Default settings for menu items
columns: 3, // Number of columns in mega menu
width: 'auto', // Menu width
alignment: 'left', // Menu alignment
animation: 'slide' // Animation type
},
autoInjectClasses: true, // Automatically inject CSS classes
autoInjectId: true, // Automatically inject element ID
debug: false // Enable verbose logging
};The configuration system follows this priority order:
- Code Injection (Highest Priority) -
window.themeixMenuConfig - External JSON File - Loaded from
jsonConfigPath - Initialization Config - Passed to
ThemeixMenu.init() - Default Configuration - Built-in defaults
The menu system integrates with Ghost's navigation structure using dash prefixes to indicate nesting levels:
Home
Products
- Software
-- Applications
--- Mobile Apps
- Hardware
Services
About Us
Each dash prefix represents one nesting level:
- No dash: Top-level menu item
-: First-level submenu--: Second-level submenu---: Third-level submenu
- Link: Simple menu item without children
- Dropdown: Menu item with basic dropdown submenu
- Mega: Large multi-column menu with grouped content
// In Ghost Code Injection (Site Footer)
<script>
window.themeixMenuConfig = {
useGhostChildren: false, // Use only JSON, ignore Ghost navigation
globalSettings: {
mobileBreakpoint: 768,
animationDuration: 300,
defaultMenuSettings: {
columns: 4,
width: '900px',
alignment: 'center',
animation: 'slide'
}
},
menus: [
{
match: {
url: '/products',
title: 'Products',
pattern: '^/product.*'
},
type: 'mega',
icon: 'star',
badge: 'New',
settings: {
columns: 3,
width: '1000px',
alignment: 'center',
animation: 'slide'
},
groups: [
{
title: 'Software',
links: [
{
title: 'Applications',
url: '/products/applications',
icon: 'book',
badge: 'Popular',
description: 'Our main applications'
},
{
title: 'Mobile Apps',
url: '/products/mobile',
icon: 'search',
description: 'iOS and Android apps'
}
]
},
{
title: 'Hardware',
links: [
{
title: 'Devices',
url: '/products/devices',
icon: 'settings'
}
]
}
]
},
{
match: { url: '/services' },
type: 'dropdown',
settings: {
width: '300px',
alignment: 'right',
animation: 'fade'
}
}
]
};
</script>useGhostChildren(boolean): Whether to merge with Ghost navigation or use only JSONglobalSettings(object): Global settings applied to all menusmenus(array): Array of menu configuration objects
match(object): Criteria for matching Ghost navigation itemsurl(string): Match by URL pathtitle(string): Match by menu item titlelabel(string): Match by menu item labelpattern(string): Regular expression pattern for URL matching
type(string): Menu type - 'link', 'dropdown', or 'mega'icon(string|object): Icon configurationbadge(string): Badge textdescription(string): Menu item descriptionsettings(object): Menu-specific settingsgroups(array): Group configuration for mega menus
The menu system includes several built-in SVG icons:
const availableIcons = [
'book', // Documentation/book icon
'megaphone', // Announcement/marketing icon
'palette', // Design/creative icon
'star', // Rating/favorite icon
'heart', // Like/love icon
'search', // Search functionality icon
'user', // User/account icon
'settings' // Settings/configuration icon
];{
icon: 'star'
}{
icon: {
sprite: '/assets/icons/sprite.png',
width: '20px',
height: '20px',
x: '0px',
y: '0px',
spriteSize: 'auto'
}
}{
icon: {
image: '/assets/icons/custom-icon.png',
width: '16px',
height: '16px',
alt: 'Custom Icon'
}
}:root {
--tdgm-menu-width: 100%; /* Main menu width */
--tdgm-menu-gap: 2rem; /* Gap between menu items */
--tdgm-menu-radius: 8px; /* Border radius */
--tdgm-menu-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); /* Box shadow */
--tdgm-menu-background: #ffffff; /* Background color */
--tdgm-menu-color: #333333; /* Text color */
--tdgm-menu-hover-color: #666666; /* Hover text color */
--tdgm-menu-active-color: #000000; /* Active text color */
/* Spacing */
--menu-padding: 0.75rem 1rem; /* Menu item padding */
--menu-font-size: inherit; /* Font size */
--menu-font-weight: 500; /* Font weight */
--menu-border: 1px solid #e5e5e5; /* Border styling */
--menu-transition: all 0.3s ease; /* Transition effect */
/* Gaps */
--tdgm-menu-gap-items: 0.5rem; /* Gap between items */
--tdgm-menu-gap-header: 1.5rem; /* Gap for headers */
--tdgm-menu-gap-group: 2rem; /* Gap between groups */
}:root {
--tdgm-badge-background: #ff6b6b; /* Badge background */
--tdgm-badge-color: #ffffff; /* Badge text color */
--tdgm-badge-padding: 0.25rem 0.5rem; /* Badge padding */
--tdgm-badge-radius: 4px; /* Badge border radius */
--tdgm-badge-font-size: 0.75rem; /* Badge font size */
}:root {
--tdgm-icon-size: 1.25rem; /* Icon size */
--tdgm-icon-gap: 0.5rem; /* Gap between icon and text */
--tdgm-icon-color: inherit; /* Icon color */
--tdgm-icon-hover-color: inherit; /* Icon hover color */
}:root {
--tdgm-dropdown-width: 250px; /* Dropdown width */
--tdgm-mega-width: 700px; /* Mega menu width */
--tdgm-submenu-background: #f8f8f8; /* Submenu background */
--tdgm-submenu-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); /* Submenu shadow */
--tdgm-submenu-border-radius: 8px; /* Submenu border radius */
--tdgm-submenu-padding: 1rem; /* Submenu padding */
--tdgm-submenu-hover-background: #f8f9fa; /* Hover background */
--tdgm-submenu-border: 1px solid #e5e5e5; /* Submenu border */
--tdgm-submenu-link-padding: 0.5rem 0.75rem; /* Link padding */
--tdgm-submenu-link-gap: 0.25rem; /* Link gap */
--tdgm-submenu-hover-color: var(--ghost-accent-color); /* Hover color */
}:root {
--tdgm-arrow-size: 0.3em; /* Arrow size */
--tdgm-arrow-width: 0.3em; /* Arrow width */
--tdgm-arrow-color: currentColor; /* Arrow color */
--tdgm-arrow-hover-color: inherit; /* Arrow hover color */
--tdgm-arrow-transform: rotate(180deg); /* Arrow rotation */
--tdgm-arrow-transition: transform 0.3s ease; /* Arrow transition */
}:root {
--tdgm-mobile-menu-background: #ffffff; /* Mobile menu background */
--tdgm-mobile-menu-width: 300px; /* Mobile menu width */
--tdgm-mobile-menu-padding: 1rem; /* Mobile menu padding */
--tdgm-mobile-transition: all 0.3s ease; /* Mobile transition */
}The menu system supports four animation types:
settings: {
animation: 'fade'
}Smooth fade-in/fade-out effect with opacity transitions.
settings: {
animation: 'slide'
}Slides submenu down from top with vertical translation.
settings: {
animation: 'scale'
}Scales submenu from 0.95 to 1.0 with scaling effect.
settings: {
animation: 'flip'
}3D flip animation using perspective transforms.
settings: {
alignment: 'left'
}Aligns submenu to the left edge of the parent item.
settings: {
alignment: 'center'
}Centers submenu relative to the viewport (uses fixed positioning).
settings: {
alignment: 'right'
}Aligns submenu to the right edge of the parent item.
{
type: 'mega',
settings: {
columns: 4, // Number of columns
width: '1200px', // Total width
alignment: 'center' // Alignment
},
groups: [
{
title: 'Group Title',
links: [
{
title: 'Link Title',
url: '/link-url',
icon: 'icon-name',
badge: 'Badge',
description: 'Link description'
}
]
}
]
}The system supports responsive column layouts:
/* 2 columns */
.mega-2-columns .themeix-mega-group {
flex: 0 0 calc(50% - 1rem);
}
/* 3 columns */
.mega-3-columns .themeix-mega-group {
flex: 0 0 calc(33.333% - 1.33rem);
}
/* 4 columns */
.mega-4-columns .themeix-mega-group {
flex: 0 0 calc(25% - 1.5rem);
}
/* 5 columns */
.mega-5-columns .themeix-mega-group {
flex: 0 0 calc(20% - 1.6rem);
}Initialize the menu system with optional configuration.
ThemeixMenu.init({
targetElement: '.gh-navigation-menu',
mobileBreakpoint: 768,
debug: true
});Destroy the menu system and clean up event listeners.
ThemeixMenu.destroy();Re-initialize the menu system (useful for dynamic content).
ThemeixMenu.refresh();Open a specific menu by ID.
ThemeixMenu.open('menu-0');Close a specific menu by ID, or all menus if no ID provided.
// Close specific menu
ThemeixMenu.close('menu-0');
// Close all menus
ThemeixMenu.close();Subscribe to menu events.
ThemeixMenu.on('menu:init', (data) => {
console.log('Menu initialized:', data);
});Unsubscribe from menu events.
ThemeixMenu.off('menu:init', callback);Emit custom events (mainly for internal use).
ThemeixMenu.emit('custom:event', { data: 'value' });Get current configuration.
const config = ThemeixMenu.getConfig();
console.log('Current config:', config);Get all menu items.
const menus = ThemeixMenu.getMenus();
console.log('All menus:', menus);Get a specific menu by ID.
const menu = ThemeixMenu.getMenu('menu-0');
console.log('Menu:', menu);menu:init: Fired when menu system is initialized- Custom events can be added via the
on()method
// Listen for menu initialization
ThemeixMenu.on('menu:init', (data) => {
console.log('Menu initialized with', data.menus.length, 'items');
});
// Custom event handling
ThemeixMenu.on('menu:opened', (menuId) => {
console.log('Menu opened:', menuId);
});The menu system supports comprehensive keyboard navigation:
- Arrow Down: Move focus to next item
- Arrow Up: Move focus to previous item
- Arrow Right: Open submenu
- Arrow Left: Close submenu
- Escape: Close all open menus
Automatic ARIA attribute management:
aria-haspopup: Indicates menu has popup contentaria-expanded: Indicates popup statearia-label: Provides descriptive labels
<a href="/products" aria-haspopup="true" aria-expanded="false" aria-label="Products">
Products
</a>The system manages focus states for keyboard accessibility:
// Focus can be managed through keyboard events
document.addEventListener('keydown', handleKeyboard);The menu system switches between desktop and mobile modes at the configured breakpoint:
{
mobileBreakpoint: 768 // Switch to mobile at 768px
}On mobile devices:
- Hover interactions are replaced with click/tap
- Submenus are positioned relatively
- Menus take full width
- Touch events are optimized to prevent double-tap delays
@media (max-width: 767px) {
.themeix-menu-container {
display: none;
}
.themeix-submenu {
position: relative !important;
width: 100% !important;
left: 0 !important;
right: 0 !important;
transform: none !important;
margin: 0 !important;
box-shadow: none !important;
}
.themeix-mega-groups {
flex-direction: column;
gap: 1rem;
padding: 0;
}
.themeix-mega-group {
min-width: 100% !important;
max-width: 100%;
}
}The menu system uses several optimization techniques:
- WeakMap for Timer Storage: Prevents memory leaks from hover timers
- Map for Menu Lookup: O(1) menu item retrieval instead of O(n) array search
- Regex Compilation: Compiled once during initialization instead of per-item
// Efficient memory usage
const hoverTimers = new WeakMap();
let menusById = new Map();
let childPrefixRegex = null; // Compiled onceEfficient event handling through delegation and proper cleanup:
// Event listeners are properly managed
function destroy() {
document.removeEventListener('click', handleClickOutside);
document.removeEventListener('keydown', handleKeyboard);
// Clean up all listeners
}Configuration is loaded asynchronously when needed:
async function loadConfiguration() {
// Priority 1: Code Injection
if (window.themeixMenuConfig) {
return window.themeixMenuConfig;
}
// Priority 2: JSON file
if (config.jsonConfigPath) {
return await loadJSONConfig(config.jsonConfigPath);
}
return null;
}- Chrome/Edge (latest)
- Firefox (latest)
- Safari (latest)
- Mobile browsers (iOS Safari, Chrome Mobile)
- ES6+ JavaScript support
- CSS Grid and Flexbox
- CSS Custom Properties (variables)
- DOM Level 3 Events
For older browsers, you may need polyfills for:
Promise(for async/await)WeakMap(for timer storage)- CSS Custom Properties
Enable debug mode for detailed logging:
ThemeixMenu.init({
debug: true
});Problem: Menu doesn't appear or work Solution:
- Check that the target element exists:
document.querySelector('.gh-navigation-menu') - Verify CSS is loaded properly
- Check browser console for errors
Problem: Submenus don't open on hover/click Solution:
- Verify menu items have children (using dash prefixes)
- Check that
childPrefixmatches your Ghost navigation structure - Ensure CSS variables are properly defined
Problem: Mobile menu doesn't work correctly Solution:
- Check
mobileBreakpointsetting - Verify touch events are working
- Check that mobile-specific CSS is loaded
Problem: JSON configuration doesn't affect menu Solution:
- Check configuration priority (Code Injection > JSON file > init config)
- Verify JSON structure is correct
- Enable debug mode to see configuration loading
// Update menu configuration dynamically
function updateMenuConfiguration(newConfig) {
ThemeixMenu.destroy();
ThemeixMenu.init(newConfig);
}// Listen for menu state changes
ThemeixMenu.on('menu:init', handleMenuInit);
ThemeixMenu.on('menu:opened', handleMenuOpen);
ThemeixMenu.on('menu:closed', handleMenuClose);
function handleMenuInit(data) {
console.log('Menu system initialized');
// Perform initialization actions
}
function handleMenuOpen(menuId) {
console.log('Menu opened:', menuId);
// Track analytics or perform other actions
}
function handleMenuClose(menuId) {
console.log('Menu closed:', menuId);
// Clean up resources
}// Integrate with analytics
ThemeixMenu.on('menu:opened', (menuId) => {
if (typeof gtag !== 'undefined') {
gtag('event', 'menu_open', {
'menu_id': menuId
});
}
});
// Integrate with state management
if (typeof store !== 'undefined') {
ThemeixMenu.on('menu:opened', (menuId) => {
store.dispatch({
type: 'MENU_OPENED',
payload: { menuId }
});
});
}- Minimize DOM Manipulation: Use the menu system's built-in methods
- Debounce Resize Events: The system already debounces resize handlers
- Use Efficient Selectors: The system uses optimized selectors internally
- Test Keyboard Navigation: Ensure all menu items are accessible via keyboard
- Check ARIA Attributes: Verify proper ARIA attributes are applied
- Test Screen Readers: Ensure compatibility with screen readers
- Keep Configuration Updated: Regularly update menu configuration
- Monitor Performance: Use debug mode to monitor performance
- Test Across Devices: Test on various screen sizes and devices
MIT License - Copyright 2026 Themeix
- GitHub Issues: https://github.com/themeix/ghost-dynamic-dropdown-mega-menu/issues
- Contact: https://themeix.com/contact
- Complete rewrite with modern JavaScript
- Improved performance with WeakMap and Map data structures
- Enhanced accessibility features
- Better mobile responsiveness
- Support for mega menus with column layouts
- Multiple animation types
- Comprehensive configuration system
- Debug mode for troubleshooting
Developed by Themeix (https://themeix.com) for the Ghost CMS community.
For detailed implementation examples and additional documentation, visit the project repository.