Bootstrap 5 + jQuery compatible multi-level flyout menu for widget integration with full dark mode support.
css/widget-flyout.css- Flyout menu styles with dark mode supportjs/widget-flyout.js- Flyout menu functionality with theme utilitiesindex.html- Main demo page with live examples and theme toggleexample-dark-mode.html- Dedicated dark mode demonstrationREADME.md- This file (complete documentation)
- ✅ Unlimited nesting depth
- ✅ RTL/LTR support via
dirattribute - ✅ Touch/hover detection (hover on desktop, click on mobile)
- ✅ Fixed positioning (submenus never hidden behind content)
- ✅ Keyboard navigation (Arrow keys, Enter, Escape)
- ✅ ARIA attributes for accessibility
- ✅ JSON data or pre-rendered HTML modes
- ✅ Multiple widgets per page (no conflicts)
- ✅ One submenu open at a time (siblings auto-close)
- ✅ Viewport edge detection
- ✅ Dark mode support with Bootstrap theme integration
- Quick Start
- Dark Mode
- API Reference
- RTL Support
- Keyboard Navigation
- Customization
- Advanced Usage
- Troubleshooting
- Browser Support
- Examples
<!-- Bootstrap 5 CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<!-- Bootstrap Icons -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- Widget Flyout CSS -->
<link href="css/widget-flyout.css" rel="stylesheet">
<!-- jQuery 3.7+ -->
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<!-- Bootstrap 5 JS Bundle -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<!-- Widget Flyout JS -->
<script src="js/widget-flyout.js"></script><div class="widget" dir="ltr">
<div class="widget-header bg-danger text-white d-flex justify-content-between align-items-center px-3 py-2">
<span class="widget-title">Categories</span><i class="bi bi-grid-3x3-gap"></i>
</div>
<div class="widget-body p-3" id="my-menu"></div>
</div>Option A: JSON Data
const data = [
{
label: 'Electronics',
icon: 'bi bi-laptop',
url: '#',
children: [
{ label: 'Laptops', icon: 'bi bi-laptop', url: '/laptops' },
{ label: 'Phones', icon: 'bi bi-phone', url: '/phones' }
]
},
{ label: 'Books', icon: 'bi bi-book', url: '/books' }
];
renderFlyoutFromJson('#my-menu', data);Option B: Existing HTML
initWidgetFlyout('#my-menu');// Initialize theme (auto-detects system preference)
FlyoutTheme.init();
// Toggle between light and dark
FlyoutTheme.toggle();
// Set specific theme
FlyoutTheme.setTheme('dark'); // or 'light' or 'auto'Initialize theme from localStorage or system preference.
FlyoutTheme.init();- Checks localStorage for saved preference
- Falls back to system preference if no saved value
- Applies the theme automatically
- Returns the applied theme ('light' or 'dark')
Set a specific theme.
FlyoutTheme.setTheme('dark'); // Set dark mode
FlyoutTheme.setTheme('light'); // Set light mode
FlyoutTheme.setTheme('auto'); // Use system preferenceParameters:
theme(string): 'light', 'dark', or 'auto'
Returns: The applied theme ('light' or 'dark')
Toggle between light and dark modes.
FlyoutTheme.toggle();Returns: The new theme ('light' or 'dark')
Get the current active theme.
const currentTheme = FlyoutTheme.getTheme();
console.log(currentTheme); // 'light' or 'dark'Watch for system theme preference changes.
FlyoutTheme.watchSystemTheme();<button type="button" class="btn btn-primary" id="themeToggle">
<i class="bi bi-moon-stars-fill"></i>
</button>
<script>
$(document).ready(function() {
FlyoutTheme.init();
FlyoutTheme.watchSystemTheme();
$('#themeToggle').on('click', function() {
FlyoutTheme.toggle();
// Update icon
const theme = FlyoutTheme.getTheme();
const icon = theme === 'dark' ? 'bi-sun-fill' : 'bi-moon-stars-fill';
$(this).find('i').attr('class', `bi ${icon}`);
});
});
</script>:root {
/* Light mode colors (default) */
--flyout-bg: #ffffff;
--flyout-border-color: #dee2e6;
--flyout-text-color: #212529;
--flyout-hover-bg: #f8f9fa;
--flyout-hover-text: #212529;
--flyout-active-bg: #e9ecef;
--flyout-focus-outline: #0d6efd;
--flyout-shadow: rgba(0, 0, 0, 0.15);
}
[data-bs-theme="dark"] {
/* Dark mode colors */
--flyout-bg: #212529;
--flyout-border-color: #495057;
--flyout-text-color: #dee2e6;
--flyout-hover-bg: #343a40;
--flyout-hover-text: #ffffff;
--flyout-active-bg: #495057;
--flyout-focus-outline: #0d6efd;
--flyout-shadow: rgba(0, 0, 0, 0.5);
}/* Custom dark theme with blue tones */
[data-bs-theme="dark"] {
--flyout-bg: #1a1f35;
--flyout-border-color: #2d3555;
--flyout-text-color: #e8eaed;
--flyout-hover-bg: #2d3555;
--flyout-hover-text: #ffffff;
--flyout-active-bg: #3d4565;
--flyout-focus-outline: #4a9eff;
--flyout-shadow: rgba(0, 0, 0, 0.6);
}Render menu from JSON data.
renderFlyoutFromJson('#menu-container', menuData, {
hoverDelay: 200,
closeOnClickOutside: true,
keyboardNav: true
});Parameters:
container(string|element): Selector or DOM elementdata(array): Menu data structureoptions(object): Optional configuration
Initialize existing HTML menu structure.
initWidgetFlyout('#menu-container', {
hoverDelay: 200,
closeOnClickOutside: true,
keyboardNav: true
});{
label: 'Text', // Required - menu item text
icon: 'bi bi-icon', // Optional - icon class
url: '#', // Optional - link URL
onClick: function(item, event) {}, // Optional - click handler
children: [] // Optional - sub-items array
}{
hoverDelay: 200, // Hover delay in milliseconds
closeOnClickOutside: true, // Close menu when clicking outside
keyboardNav: true // Enable keyboard navigation
}const menuData = [
{
label: 'Home',
icon: 'bi bi-house',
url: '/home',
children: [
{
label: 'Dashboard',
icon: 'bi bi-speedometer2',
url: '/dashboard'
},
{
label: 'Settings',
icon: 'bi bi-gear',
url: '/settings',
children: [
{ label: 'Profile', icon: 'bi bi-person', url: '/profile' },
{ label: 'Security', icon: 'bi bi-shield-lock', url: '/security' }
]
}
]
},
{
label: 'Search',
icon: 'bi bi-search',
onClick: function(item, e) {
e.preventDefault();
alert('Search clicked!');
}
}
];
$(document).ready(function() {
// Initialize theme
FlyoutTheme.init();
// Render menu
renderFlyoutFromJson('#my-menu', menuData, {
hoverDelay: 150,
closeOnClickOutside: true,
keyboardNav: true
});
});<div class="widget" dir="rtl">
<div class="widget-header bg-danger text-white d-flex justify-content-between align-items-center px-3 py-2">
<span class="widget-title">دستهها</span><i class="bi bi-grid-3x3-gap"></i>
</div>
<div class="widget-body p-3" id="rtl-menu"></div>
</div>
<script>
const persianData = [
{
label: 'خانه',
icon: 'bi bi-house',
url: '#',
children: [
{ label: 'داشبورد', icon: 'bi bi-speedometer2', url: '#' }
]
}
];
renderFlyoutFromJson('#rtl-menu', persianData);
</script>The library automatically detects RTL direction from:
dir="rtl"attribute on parent elementsdir="rtl"on<html>element- Adapts caret direction and menu positioning automatically
| Key | Action (LTR) | Action (RTL) |
|---|---|---|
↓ |
Move to next item | Move to next item |
↑ |
Move to previous item | Move to previous item |
→ |
Open submenu | Close submenu |
← |
Close submenu | Open submenu |
Enter |
Activate item | Activate item |
Space |
Activate item | Activate item |
Esc |
Close current submenu | Close current submenu |
- Full ARIA attribute support
- Screen reader compatible
- Keyboard-only navigation
- Focus indicators visible
- WCAG 2.1 Level AA compliant
- Minimum contrast ratio 4.5:1 for text
/* Override menu background */
.widget-flyout-menu .dropdown-menu {
background-color: #your-color;
}
/* Custom hover effect */
.widget-flyout-menu .dropdown-item:hover {
background-color: #your-hover-color;
transform: translateX(5px);
}
/* Custom spacing */
:root {
--flyout-spacing: 5px;
}
/* Custom transition speed */
:root {
--flyout-transition: 0.3s;
}Works with any icon library:
// Bootstrap Icons
{ label: 'Home', icon: 'bi bi-house', url: '#' }
// Font Awesome
{ label: 'Home', icon: 'fas fa-home', url: '#' }
// Material Icons
{ label: 'Home', icon: 'material-icons', url: '#' }{
label: 'Export',
icon: 'bi bi-download',
onClick: function(item, event) {
event.preventDefault();
// Your custom logic
exportData();
// Close menu
$(event.target).closest('.dropdown-submenu').removeClass('show');
}
}$.ajax({
url: '/api/menu',
method: 'GET',
success: function(data) {
renderFlyoutFromJson('#menu', data);
},
error: function() {
console.error('Failed to load menu');
}
});// Clear existing menu
$('#menu').empty();
// Render new menu
renderFlyoutFromJson('#menu', newMenuData);// Initialize multiple menus independently
renderFlyoutFromJson('#menu1', data1);
renderFlyoutFromJson('#menu2', data2);
renderFlyoutFromJson('#menu3', data3);// Custom event after menu renders
$(document).on('flyout:rendered', '#menu', function() {
console.log('Menu rendered!');
});
// Item click event
$(document).on('click', '.widget-flyout-menu .dropdown-item', function(e) {
const itemText = $(this).text();
console.log('Clicked:', itemText);
});<div id="my-menu">
<ul class="widget-flyout-menu list-unstyled">
<li class="dropdown-submenu">
<a href="#" class="dropdown-toggle dropdown-item">
<i class="bi bi-house"></i>Home
</a>
<ul class="dropdown-menu">
<li><a href="/dashboard" class="dropdown-item">
<i class="bi bi-speedometer2"></i>Dashboard
</a></li>
<li class="dropdown-submenu">
<a href="#" class="dropdown-toggle dropdown-item">
<i class="bi bi-gear"></i>Settings
</a>
<ul class="dropdown-menu">
<li><a href="/profile" class="dropdown-item">
<i class="bi bi-person"></i>Profile
</a></li>
</ul>
</li>
</ul>
</li>
</ul>
</div>
<script>
initWidgetFlyout('#my-menu');
</script>Problem: Submenus don't open when clicked/hovered.
Solutions:
// 1. Check if jQuery is loaded
console.log(typeof $ !== 'undefined'); // Should be true
// 2. Check if library is loaded
console.log(typeof window.initWidgetFlyout !== 'undefined'); // Should be true
// 3. Ensure menu is initialized
initWidgetFlyout('#menu-container');
// 4. Check console for errors
// Open browser DevTools (F12) and check Console tabProblem: Theme doesn't change or persist.
Solutions:
// 1. Initialize theme
FlyoutTheme.init();
// 2. Check current theme
console.log(FlyoutTheme.getTheme());
// 3. Manually set theme
FlyoutTheme.setTheme('dark');
// 4. Check if localStorage is available
console.log(typeof(Storage) !== "undefined");
// 5. Check data-bs-theme attribute
console.log(document.documentElement.getAttribute('data-bs-theme'));Submenus Hidden Behind Content
Problem: Dropdowns appear behind other elements.
Solutions:
/* Check parent containers for overflow:hidden */
.parent-container {
overflow: visible !important;
}
/* Increase z-index if needed */
.dropdown-menu {
z-index: 10000 !important;
}Problem: Menu doesn't flip for RTL languages.
Solutions:
<!-- Ensure dir attribute is set -->
<div dir="rtl">
<div id="menu"></div>
</div>
<!-- Or on html element -->
<html dir="rtl">Problem: Icons don't show up.
Solutions:
<!-- 1. Ensure icon library is loaded -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<!-- 2. Check icon class names -->
<script>
// Correct
{ label: 'Home', icon: 'bi bi-house', url: '#' }
// Wrong - missing 'bi' prefix
{ label: 'Home', icon: 'house', url: '#' }
</script>Problem: Menus don't work on mobile/tablets.
Solution: The library automatically detects touch devices. If not working:
// Force touch device mode
$('body').addClass('touch-device');
// Then reinitialize
initWidgetFlyout('#menu');Problem: Brief flash of light theme before dark mode applies.
Solution: Add inline script in <head> before CSS:
<head>
<script>
(function() {
const theme = localStorage.getItem('flyout-theme') || 'light';
document.documentElement.setAttribute('data-bs-theme', theme);
})();
</script>
<!-- Then your CSS files -->
</head>| Browser | Version | Notes |
|---|---|---|
| Chrome | 90+ | Full support |
| Edge | 90+ | Full support |
| Firefox | 88+ | Full support |
| Safari | 14+ | Full support |
| Opera | 76+ | Full support |
| iOS Safari | 14+ | Touch optimized |
| Chrome Android | 90+ | Touch optimized |
| Browser | Support Level |
|---|---|
| IE 11 | |
| Edge Legacy |
The library includes automatic feature detection for:
- Touch devices
prefers-color-schemesupport- localStorage availability
- matchMedia API
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Simple Menu</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/widget-flyout.css" rel="stylesheet">
</head>
<body>
<div class="widget">
<div class="widget-header bg-primary text-white px-3 py-2">
<span>Menu</span>
</div>
<div class="widget-body p-3" id="menu"></div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/widget-flyout.js"></script>
<script>
const data = [
{ label: 'Home', icon: 'bi bi-house', url: '/' },
{ label: 'About', icon: 'bi bi-info-circle', url: '/about' },
{ label: 'Contact', icon: 'bi bi-envelope', url: '/contact' }
];
$(document).ready(function() {
renderFlyoutFromJson('#menu', data);
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Menu with Dark Mode</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/widget-flyout.css" rel="stylesheet">
<style>
body {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
transition: all 0.3s ease;
padding: 2rem;
}
</style>
</head>
<body>
<button class="btn btn-primary mb-3" id="toggleTheme">Toggle Dark Mode</button>
<div class="widget">
<div class="widget-header bg-success text-white px-3 py-2">
<span>Categories</span>
</div>
<div class="widget-body p-3" id="menu"></div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/widget-flyout.js"></script>
<script>
const data = [
{
label: 'Electronics',
icon: 'bi bi-laptop',
children: [
{ label: 'Computers', icon: 'bi bi-pc-display', url: '/computers' },
{ label: 'Phones', icon: 'bi bi-phone', url: '/phones' }
]
},
{
label: 'Books',
icon: 'bi bi-book',
url: '/books'
}
];
$(document).ready(function() {
FlyoutTheme.init();
renderFlyoutFromJson('#menu', data);
$('#toggleTheme').on('click', function() {
FlyoutTheme.toggle();
});
});
</script>
</body>
</html><!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<title>منوی فارسی</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.1/font/bootstrap-icons.css" rel="stylesheet">
<link href="css/widget-flyout.css" rel="stylesheet">
</head>
<body style="padding: 2rem;">
<div class="widget">
<div class="widget-header bg-danger text-white px-3 py-2">
<span>دستهها</span>
</div>
<div class="widget-body p-3" id="menu"></div>
</div>
<script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="js/widget-flyout.js"></script>
<script>
const data = [
{
label: 'خانه',
icon: 'bi bi-house',
children: [
{ label: 'داشبورد', icon: 'bi bi-speedometer2', url: '/dashboard' },
{ label: 'تنظیمات', icon: 'bi bi-gear', url: '/settings' }
]
},
{ label: 'درباره ما', icon: 'bi bi-info-circle', url: '/about' }
];
$(document).ready(function() {
renderFlyoutFromJson('#menu', data);
});
</script>
</body>
</html>Open index.html to see:
- LTR menus (left sidebar)
- RTL menus (right sidebar)
- JSON and HTML initialization
- Dark mode toggle
- Keyboard navigation
- Touch/hover behavior
Open example-dark-mode.html to see:
- Light/Dark/Auto theme switching
- Theme API demonstration
- CSS variables showcase
- Multiple menus in dark mode
- localStorage persistence
- CSS file size: ~3 KB (minified)
- JS file size: ~5 KB (minified)
- Runtime overhead: <1ms
- Theme switch time: <50ms
- No layout recalculation on theme change (uses CSS variables)
- No external API calls
- No user tracking
- localStorage only for theme preference
- No cookies used
- XSS protection maintained
- Safe error handling throughout
- ✅ Minimum contrast ratio 4.5:1 for text
- ✅ Minimum contrast ratio 3:1 for UI components
- ✅ Focus indicators visible in all modes
- ✅ Keyboard navigation fully supported
- ✅ ARIA attributes on all interactive elements
- ✅ Screen reader compatible
- ✅ No information conveyed by color alone
- ✅ Touch targets minimum 44x44px
Provided as-is for use in your projects.
index.html- Main demonstrationexample-dark-mode.html- Dark mode examples
Paste this in browser console to test:
const testData = [
{ label: 'Test 1', url: '#1' },
{ label: 'Test 2', url: '#2', children: [
{ label: 'Sub 1', url: '#s1' }
]}
];
renderFlyoutFromJson('#menu', testData);
FlyoutTheme.toggle();Version: 2.0.0
Last Updated: November 2025
Status: ✅ Production Ready