<script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
<script nomodule src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.js"></script>
<div class="container">
<h1>Accessible Dropdown Menus</h1>
<p class="subtitle">Keyboard-navigable dropdowns following WAI-ARIA best practices</p>
<div class="demo-grid">
<!-- Navigation Dropdown -->
<div class="demo-section">
<h2>
<ion-icon name="menu-outline"></ion-icon>
Navigation Menu
</h2>
<p class="description">
Use Arrow keys to navigate, Enter to select, Escape to close
</p>
<nav class="nav-container">
<ul class="navbar" role="menubar">
<li class="nav-item" role="none">
<button
class="nav-button"
id="productsBtn"
role="menuitem"
aria-haspopup="true"
aria-expanded="false"
aria-controls="productsMenu"
>
Products
<ion-icon name="chevron-down-outline"></ion-icon>
</button>
<ul class="dropdown-menu" id="productsMenu" role="menu" aria-labelledby="productsBtn">
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem">
<ion-icon name="laptop-outline"></ion-icon>
Software
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem">
<ion-icon name="hardware-chip-outline"></ion-icon>
Hardware
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem">
<ion-icon name="cloud-outline"></ion-icon>
Cloud Services
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem">
<ion-icon name="shield-checkmark-outline"></ion-icon>
Security
</button>
</li>
</ul>
</li>
</ul>
</nav>
</div>
<!-- Select Dropdown -->
<div class="demo-section">
<h2>
<ion-icon name="list-outline"></ion-icon>
Custom Select
</h2>
<p class="description">
Use Arrow keys to navigate, Enter to select, Escape to close
</p>
<div class="select-container">
<button
class="select-button"
id="countrySelect"
role="combobox"
aria-haspopup="listbox"
aria-expanded="false"
aria-controls="countryList"
aria-labelledby="countryLabel"
>
<span id="selectedCountry">Select a country</span>
<ion-icon name="chevron-down-outline"></ion-icon>
</button>
<ul class="select-dropdown" id="countryList" role="listbox">
<li class="select-option" role="option" tabindex="-1" data-value="us">
<ion-icon name="checkmark-outline"></ion-icon>
United States
</li>
<li class="select-option" role="option" tabindex="-1" data-value="uk">
<ion-icon name="checkmark-outline"></ion-icon>
United Kingdom
</li>
<li class="select-option" role="option" tabindex="-1" data-value="ca">
<ion-icon name="checkmark-outline"></ion-icon>
Canada
</li>
<li class="select-option" role="option" tabindex="-1" data-value="au">
<ion-icon name="checkmark-outline"></ion-icon>
Australia
</li>
<li class="select-option" role="option" tabindex="-1" data-value="de">
<ion-icon name="checkmark-outline"></ion-icon>
Germany
</li>
<li class="select-option" role="option" tabindex="-1" data-value="fr">
<ion-icon name="checkmark-outline"></ion-icon>
France
</li>
</ul>
</div>
</div>
<!-- Action Menu -->
<div class="demo-section">
<h2>
<ion-icon name="ellipsis-horizontal-outline"></ion-icon>
Action Menu
</h2>
<p class="description">
Context menu with keyboard support
</p>
<button
class="action-button"
id="actionsBtn"
aria-haspopup="true"
aria-expanded="false"
aria-controls="actionsMenu"
>
<ion-icon name="settings-outline"></ion-icon>
Actions
</button>
<div style="position: relative;">
<ul class="dropdown-menu" id="actionsMenu" role="menu" aria-labelledby="actionsBtn">
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem" data-action="edit">
<ion-icon name="create-outline"></ion-icon>
Edit
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem" data-action="duplicate">
<ion-icon name="copy-outline"></ion-icon>
Duplicate
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem" data-action="share">
<ion-icon name="share-social-outline"></ion-icon>
Share
</button>
</li>
<li class="dropdown-item" role="none">
<button class="dropdown-link" role="menuitem" data-action="delete">
<ion-icon name="trash-outline"></ion-icon>
Delete
</button>
</li>
</ul>
</div>
<div class="status-text" id="actionStatus" role="status" aria-live="polite"></div>
</div>
</div>
</div>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 1000px;
margin: 0 auto;
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
}
h1 {
color: #2c3e50;
margin-bottom: 10px;
font-size: 32px;
}
.subtitle {
color: #7f8c8d;
margin-bottom: 40px;
font-size: 15px;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 30px;
margin-top: 30px;
}
.demo-section {
background: #f8f9fa;
padding: 25px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
h2 {
color: #34495e;
margin-bottom: 15px;
font-size: 18px;
display: flex;
align-items: center;
gap: 10px;
}
.description {
color: #6c757d;
font-size: 14px;
margin-bottom: 20px;
line-height: 1.5;
}
/* Navigation Dropdown */
.nav-container {
background: #2c3e50;
border-radius: 6px;
padding: 0;
}
.navbar {
list-style: none;
display: flex;
align-items: center;
}
.nav-item {
position: relative;
}
.nav-button {
background: transparent;
border: none;
color: white;
padding: 16px 20px;
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: background 0.2s;
}
.nav-button:hover {
background: rgba(255,255,255,0.1);
}
.nav-button:focus {
outline: 2px solid #3498db;
outline-offset: -2px;
}
.nav-button ion-icon {
transition: transform 0.2s;
}
.nav-button[aria-expanded="true"] ion-icon {
transform: rotate(180deg);
}
.dropdown-menu {
position: absolute;
top: 100%;
left: 0;
background: white;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
list-style: none;
min-width: 200px;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s;
z-index: 1000;
margin-top: 5px;
}
.dropdown-menu.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.dropdown-item {
margin: 0;
}
.dropdown-link {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
color: #2c3e50;
text-decoration: none;
transition: background 0.2s;
border: none;
background: transparent;
width: 100%;
text-align: left;
font-size: 14px;
cursor: pointer;
}
.dropdown-link:hover {
background: #f8f9fa;
}
.dropdown-link:focus {
outline: 2px solid #3498db;
outline-offset: -2px;
background: #e9ecef;
}
.dropdown-item:first-child .dropdown-link {
border-radius: 6px 6px 0 0;
}
.dropdown-item:last-child .dropdown-link {
border-radius: 0 0 6px 6px;
}
/* Select Dropdown */
.select-container {
position: relative;
}
.select-button {
width: 100%;
padding: 12px 16px;
background: white;
border: 2px solid #dee2e6;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
transition: border-color 0.2s;
}
.select-button:hover {
border-color: #3498db;
}
.select-button:focus {
outline: none;
border-color: #3498db;
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
}
.select-button ion-icon {
transition: transform 0.2s;
}
.select-button[aria-expanded="true"] ion-icon {
transform: rotate(180deg);
}
.select-dropdown {
position: absolute;
top: calc(100% + 5px);
left: 0;
right: 0;
background: white;
border: 2px solid #dee2e6;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
list-style: none;
max-height: 250px;
overflow-y: auto;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.2s;
z-index: 1000;
}
.select-dropdown.show {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.select-option {
padding: 12px 16px;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 10px;
}
.select-option:hover {
background: #f8f9fa;
}
.select-option:focus {
outline: 2px solid #3498db;
outline-offset: -2px;
background: #e9ecef;
}
.select-option[aria-selected="true"] {
background: #e3f2fd;
color: #1976d2;
font-weight: 500;
}
.select-option ion-icon {
opacity: 0;
}
.select-option[aria-selected="true"] ion-icon {
opacity: 1;
}
/* Action Menu */
.action-button {
padding: 12px 20px;
background: #3498db;
color: white;
border: none;
border-radius: 6px;
font-size: 15px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 8px;
transition: background 0.2s;
}
.action-button:hover {
background: #2980b9;
}
.action-button:focus {
outline: 3px solid rgba(52, 152, 219, 0.4);
outline-offset: 2px;
}
.status-text {
margin-top: 15px;
padding: 12px;
background: #d4edda;
border: 1px solid #c3e6cb;
border-radius: 6px;
color: #155724;
font-size: 14px;
display: none;
}
.status-text.show {
display: block;
}
@media (max-width: 768px) {
.demo-grid {
grid-template-columns: 1fr;
}
}
// Navigation Dropdown
const productsBtn = document.getElementById('productsBtn');
const productsMenu = document.getElementById('productsMenu');
const productLinks = productsMenu.querySelectorAll('.dropdown-link');
function toggleNavMenu() {
const isExpanded = productsBtn.getAttribute('aria-expanded') === 'true';
productsBtn.setAttribute('aria-expanded', !isExpanded);
productsMenu.classList.toggle('show');
if (!isExpanded) {
productLinks[0].focus();
}
}
productsBtn.addEventListener('click', toggleNavMenu);
productsBtn.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
toggleNavMenu();
}
});
productLinks.forEach((link, index) => {
link.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = productLinks[index + 1] || productLinks[0];
next.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = productLinks[index - 1] || productLinks[productLinks.length - 1];
prev.focus();
} else if (e.key === 'Escape') {
productsBtn.setAttribute('aria-expanded', 'false');
productsMenu.classList.remove('show');
productsBtn.focus();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
alert(`Selected: ${link.textContent.trim()}`);
productsBtn.setAttribute('aria-expanded', 'false');
productsMenu.classList.remove('show');
}
});
link.addEventListener('click', () => {
alert(`Selected: ${link.textContent.trim()}`);
productsBtn.setAttribute('aria-expanded', 'false');
productsMenu.classList.remove('show');
});
});
// Custom Select Dropdown
const countrySelect = document.getElementById('countrySelect');
const countryList = document.getElementById('countryList');
const countryOptions = countryList.querySelectorAll('.select-option');
const selectedCountry = document.getElementById('selectedCountry');
let currentIndex = -1;
function toggleSelect() {
const isExpanded = countrySelect.getAttribute('aria-expanded') === 'true';
countrySelect.setAttribute('aria-expanded', !isExpanded);
countryList.classList.toggle('show');
if (!isExpanded && currentIndex >= 0) {
countryOptions[currentIndex].focus();
}
}
countrySelect.addEventListener('click', toggleSelect);
countrySelect.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
if (countrySelect.getAttribute('aria-expanded') === 'false') {
toggleSelect();
}
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (countrySelect.getAttribute('aria-expanded') === 'false') {
toggleSelect();
}
}
});
countryOptions.forEach((option, index) => {
option.addEventListener('click', () => {
selectOption(index);
});
option.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = countryOptions[index + 1] || countryOptions[0];
next.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = countryOptions[index - 1] || countryOptions[countryOptions.length - 1];
prev.focus();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
selectOption(index);
} else if (e.key === 'Escape') {
countrySelect.setAttribute('aria-expanded', 'false');
countryList.classList.remove('show');
countrySelect.focus();
}
});
});
function selectOption(index) {
countryOptions.forEach(opt => opt.setAttribute('aria-selected', 'false'));
countryOptions[index].setAttribute('aria-selected', 'true');
selectedCountry.textContent = countryOptions[index].textContent.trim();
currentIndex = index;
countrySelect.setAttribute('aria-expanded', 'false');
countryList.classList.remove('show');
countrySelect.focus();
}
// Action Menu
const actionsBtn = document.getElementById('actionsBtn');
const actionsMenu = document.getElementById('actionsMenu');
const actionLinks = actionsMenu.querySelectorAll('.dropdown-link');
const actionStatus = document.getElementById('actionStatus');
function toggleActionsMenu() {
const isExpanded = actionsBtn.getAttribute('aria-expanded') === 'true';
actionsBtn.setAttribute('aria-expanded', !isExpanded);
actionsMenu.classList.toggle('show');
if (!isExpanded) {
actionLinks[0].focus();
}
}
actionsBtn.addEventListener('click', toggleActionsMenu);
actionLinks.forEach((link, index) => {
link.addEventListener('keydown', (e) => {
if (e.key === 'ArrowDown') {
e.preventDefault();
const next = actionLinks[index + 1] || actionLinks[0];
next.focus();
} else if (e.key === 'ArrowUp') {
e.preventDefault();
const prev = actionLinks[index - 1] || actionLinks[actionLinks.length - 1];
prev.focus();
} else if (e.key === 'Escape') {
actionsBtn.setAttribute('aria-expanded', 'false');
actionsMenu.classList.remove('show');
actionsBtn.focus();
} else if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
executeAction(link);
}
});
link.addEventListener('click', () => {
executeAction(link);
});
});
function executeAction(link) {
const action = link.getAttribute('data-action');
actionStatus.textContent = `Action executed: ${action.charAt(0).toUpperCase() + action.slice(1)}`;
actionStatus.classList.add('show');
actionsBtn.setAttribute('aria-expanded', 'false');
actionsMenu.classList.remove('show');
setTimeout(() => {
actionStatus.classList.remove('show');
}, 3000);
}
// Close dropdowns when clicking outside
document.addEventListener('click', (e) => {
if (!productsBtn.contains(e.target) && !productsMenu.contains(e.target)) {
productsBtn.setAttribute('aria-expanded', 'false');
productsMenu.classList.remove('show');
}
if (!countrySelect.contains(e.target) && !countryList.contains(e.target)) {
countrySelect.setAttribute('aria-expanded', 'false');
countryList.classList.remove('show');
}
if (!actionsBtn.contains(e.target) && !actionsMenu.contains(e.target)) {
actionsBtn.setAttribute('aria-expanded', 'false');
actionsMenu.classList.remove('show');
}
});
No comments yet. Be the first!