Cursor rule
.cursor/rules/dropdown-navigation-accessibility.mdcDropdown Navigation component accessibility compliance pattern
Cursor rules
Quality
36/100
Scores the file, not the repository.Length
1,299 words
1 headings · 5 code blocksRepository
428
— · pushed 17 days agoLast changed
3 days ago
First indexed 3 days ago.123456# Dropdown Navigation Component Accessibility Standards78Ensures dropdown navigation components follow WCAG compliance and proper navigation semantics, including mobile modal patterns and disclosure controls.910<rule>11name: dropdown_navigation_accessibility_standards12description: Enforce dropdown navigation component accessibility standards and proper navigation semantics13filters:14 - type: file_extension15 pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"1617actions:18 - type: enforce19 conditions:20 # Navigation landmark requirement21 - pattern: "(?i)<nav[^>]*(?:navigation|menu|dropdown)[^>]*>"22 pattern_negate: "(aria-label|aria-labelledby)=\"[^\"]+\""23 message: "Navigation elements must have aria-label or aria-labelledby attribute for accessibility."2425 # Navigation list structure requirement26 - pattern: "(?i)<nav[^>]*(?:navigation|menu|dropdown)[^>]*>"27 pattern_negate: "<ul[^>]*>"28 message: "Navigation should use unordered list (ul) for proper semantic structure."2930 # Dropdown button role requirement31 - pattern: "(?i)<(button|div|span)[^>]*(?:dropdown|expand|collapse)[^>]*>"32 pattern_negate: "role=\"button\""33 message: "Dropdown controls must have role='button' (or use native button element which has implicit role)."3435 # Dropdown button aria-expanded requirement36 - pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:dropdown|expand|collapse)[^>]*>"37 pattern_negate: "aria-expanded=\"(true|false)\""38 message: "Dropdown controls must have aria-expanded attribute set to 'true' or 'false'."3940 # Dropdown content missing proper identification41 - pattern: "(?i)<(div|section)[^>]*(?:dropdown.*content|content.*dropdown)[^>]*>"42 pattern_negate: "id=\"[^\"]+\""43 message: "Dropdown content must have unique ID attributes for aria-controls reference."4445 # Missing aria-current on navigation items46 - pattern: "(?i)<a[^>]*(?:nav|navigation)[^>]*>"47 pattern_negate: "aria-current=\"(page|false)\""48 message: "Navigation links should have aria-current attribute set to 'page' for active items or 'false' for inactive."4950 # Mobile modal missing dialog role51 - pattern: "(?i)<(div|section)[^>]*(?:mobile.*nav|nav.*mobile|modal.*nav)[^>]*>"52 pattern_negate: "role=\"dialog\""53 message: "Mobile navigation modal containers must have role='dialog' attribute."5455 # Mobile modal missing aria-modal56 - pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"57 pattern_negate: "aria-modal=\"true\""58 message: "Mobile navigation dialog elements must have aria-modal='true' attribute."5960 # Mobile modal missing proper labeling61 - pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"62 pattern_negate: "(aria-labelledby|aria-label)"63 message: "Mobile navigation dialog elements must have either aria-labelledby or aria-label for accessibility."6465 # Mobile launcher missing aria-haspopup66 - pattern: "(?i)<button[^>]*(?:mobile.*nav|nav.*mobile|hamburger|menu)[^>]*>"67 pattern_negate: "aria-haspopup=\"dialog\""68 message: "Mobile navigation launcher buttons must include aria-haspopup='dialog' to inform users a dialog will open."6970 # Mobile close button missing aria-label71 - pattern: "(?i)<button[^>]*(?:close|dismiss|×|×)[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"72 pattern_negate: "aria-label=\"[^\"]*[Cc]lose[^\"]*\""73 message: "Mobile navigation close buttons should have aria-label='Close navigation' or similar descriptive text."7475 # Missing keyboard event handlers for dropdown76 - pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:dropdown|expand|collapse)[^>]*>"77 pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"78 message: "Dropdown controls should handle keyboard events (Enter, Space, and Escape)."7980 # Missing Escape key support for dropdown content81 - pattern: "(?i)<div[^>]*(?:dropdown.*content|content.*dropdown)[^>]*>"82 pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"83 message: "Dropdown content areas should handle Escape key to close dropdown and return focus to launcher."8485 # Incorrect menu role usage86 - pattern: "(?i)role=\"(menu|menuitem|menubar|menuitemcheckbox|menuitemradio)\""87 message: "Navigation components should NOT use menu roles. Use proper navigation semantics with ul/li/a elements."8889 # Incorrect aria-haspopup usage90 - pattern: "(?i)aria-haspopup=\"(true|menu|listbox)\""91 pattern_negate: "aria-haspopup=\"dialog\""92 message: "Navigation components should NOT use aria-haspopup except for mobile modal launchers with aria-haspopup='dialog'."9394 - type: suggest95 message: |96 **Dropdown Navigation Component Accessibility Best Practices:**9798 **Navigation Semantics:**99 - **role='navigation':** Implicit on nav element, provides landmark100 - **aria-label/aria-labelledby:** On nav element to describe the navigation101 - **aria-current:** Set on active navigation items ('page' for current page, 'false' for inactive)102 - **ul + li + a:** Use semantic list structure for navigation items103 - **NO menu roles:** Do not use role="menu", role="menuitem", etc.104105 **Dropdown Disclosure Pattern:**106 - **role='button':** Set on dropdown launcher elements (or use native button)107 - **aria-expanded:** 'true' if dropdown content is visible, 'false' if hidden108 - **aria-controls:** Reference to the ID of the associated dropdown content109 - **aria-current:** Set on active dropdown items110111 **Mobile Modal Pattern:**112 - **role='dialog':** Set on mobile navigation modal container113 - **aria-modal='true':** Indicates the dialog is modal114 - **aria-labelledby/aria-label:** On mobile dialog for accessibility115 - **aria-haspopup='dialog':** Set on mobile launcher button116 - **aria-label:** Set on mobile close button117118 **Keyboard Interaction Requirements:**119 - **Tab:** Move through natural tab order on page120 - **Enter:** Activate links121 - **Space/Enter:** Open/close dropdown when launcher is focused122 - **Escape:** Close dropdown component (launcher focused or not)123 - **NO auto-open:** Do not open dropdown on keyboard focus124 - **Mobile:** Focus management for modal open/close125126 **Mouse Interaction Requirements:**127 - **Hover:** Show underline on links128 - **Hover:** Open dropdown, close on mouse-away129 - **Click:** Activate links (include mouse-specific click handlers)130 - **Mobile:** Click launcher opens modal, click close button closes modal131 - **Double-click:** Mobile dropdown launchers navigate to associated page132133 **Mobile Interaction Requirements:**134 - **Three-line button:** Opens modal, focus moves to first focusable item135 - **X close button:** Closes modal, focus returns to launcher136 - **Dropdown launcher:** Reveals content section, single-click toggles, double-click navigates137 - **Focus management:** Proper focus trapping and restoration138139 **Implementation Patterns:**140141 **Desktop Navigation Structure:**142```html143 <header>144 <nav aria-label="Main navigation">145 <ul>146 <li>147 <a href="/home" aria-current="page">Home</a>148 </li>149 <li>150 <button type="button"151 role="button"152 aria-expanded="false"153 aria-controls="products-dropdown">154 Products155 <span class="chevron">▼</span>156 </button>157 <div id="products-dropdown" hidden>158 <ul>159 <li><a href="/products/category1">Category 1</a></li>160 <li><a href="/products/category2">Category 2</a></li>161 </ul>162 </div>163 </li>164 <li>165 <a href="/about" aria-current="false">About</a>166 </li>167 </ul>168 </nav>169 </header>170```171172 **Mobile Navigation Structure:**173```html174 <header>175 <button type="button"176 aria-haspopup="dialog"177 aria-label="Open navigation menu"178 onclick="openMobileNav()">179 <span class="hamburger">☰</span>180 </button>181 </header>182183 <div role="dialog"184 aria-modal="true"185 aria-labelledby="mobile-nav-title"186 class="mobile-nav-modal"187 hidden>188 <button type="button"189 aria-label="Close navigation menu"190 onclick="closeMobileNav()">191 ×192 </button>193 <h2 id="mobile-nav-title">Navigation</h2>194 <nav aria-label="Mobile navigation">195 <ul>196 <li>197 <a href="/home" aria-current="page">Home</a>198 </li>199 <li>200 <button type="button"201 role="button"202 aria-expanded="false"203 aria-controls="mobile-products-dropdown"204 data-href="/products">205 Products206 <span class="chevron">▼</span>207 </button>208 <div id="mobile-products-dropdown" hidden>209 <ul>210 <li><a href="/products/category1">Category 1</a></li>211 <li><a href="/products/category2">Category 2</a></li>212 </ul>213 </div>214 </li>215 <li>216 <a href="/about" aria-current="false">About</a>217 </li>218 </ul>219 </nav>220 </div>221```222223 **JavaScript for Dropdown Toggle:**224```javascript225 function toggleDropdown(button) {226 const isExpanded = button.getAttribute('aria-expanded') === 'true';227 const content = document.getElementById(button.getAttribute('aria-controls'));228229 button.setAttribute('aria-expanded', !isExpanded);230 content.hidden = isExpanded;231232 // Update chevron icon233 const chevron = button.querySelector('.chevron');234 if (chevron) {235 chevron.textContent = isExpanded ? '▼' : '▲';236 }237238 if (!isExpanded) {239 // Add escape key listener to content240 content.addEventListener('keydown', handleDropdownEscapeKey);241 } else {242 // Remove escape key listener243 content.removeEventListener('keydown', handleDropdownEscapeKey);244 }245 }246247 function handleDropdownEscapeKey(event) {248 if (event.key === 'Escape') {249 const content = event.target.closest('[hidden]');250 if (content) {251 const button = document.querySelector(`[aria-controls="${content.id}"]`);252 if (button) {253 button.setAttribute('aria-expanded', 'false');254 content.hidden = true;255 button.focus(); // Return focus to launcher256 content.removeEventListener('keydown', handleDropdownEscapeKey);257 }258 }259 }260 }261```262263 **JavaScript for Mobile Modal:**264```javascript265 function openMobileNav() {266 const modal = document.querySelector('.mobile-nav-modal');267 const closeButton = modal.querySelector('button[aria-label*="Close"]');268269 modal.hidden = false;270 closeButton.focus(); // Focus first focusable element271 }272273 function closeMobileNav() {274 const modal = document.querySelector('.mobile-nav-modal');275 const launcher = document.querySelector('[aria-haspopup="dialog"]');276277 modal.hidden = true;278 launcher.focus(); // Return focus to launcher279 }280281 // Handle escape key for mobile modal282 document.addEventListener('keydown', function(event) {283 if (event.key === 'Escape') {284 const modal = document.querySelector('.mobile-nav-modal');285 if (!modal.hidden) {286 closeMobileNav();287 }288 }289 });290291 // Mobile dropdown click and double-click handlers292 document.addEventListener('DOMContentLoaded', function() {293 const mobileDropdownButtons = document.querySelectorAll('.mobile-dropdown-button');294295 mobileDropdownButtons.forEach(button => {296 // Single click toggles dropdown297 button.addEventListener('click', (event) => {298 event.preventDefault();299 toggleDropdown(button);300 });301302 // Double click navigates to page303 button.addEventListener('dblclick', (event) => {304 event.preventDefault();305 const href = button.getAttribute('data-href') || '/default';306 window.location.href = href;307 });308 });309 });310```311312 **CSS for Hover Interactions:**313```css314 /* Link hover underline */315 nav a:hover {316 text-decoration: underline;317 }318319 /* Dropdown hover behavior */320 .dropdown-launcher:hover + .dropdown-content,321 .dropdown-content:hover {322 display: block;323 }324325 .dropdown-content {326 display: none;327 }328329 /* Chevron rotation */330 .chevron {331 transition: transform 0.2s ease;332 }333334 button[aria-expanded="true"] .chevron {335 transform: rotate(180deg);336 }337```338339 **JavaScript Considerations:**340 - Implement proper event listeners for all keyboard interactions341 - Handle mouse hover events for dropdown display342 - Manage focus for mobile modal open/close343 - Update aria-current states based on current page344 - Handle chevron icon rotation345 - Implement proper escape key handling346 - Ensure no auto-opening on keyboard focus347 - Handle mouse-specific click events for navigation348 - Implement double-click navigation for mobile dropdown launchers349350 **Accessibility Notes:**351 - Navigation is NOT a menu - use proper navigation semantics352 - Avoid menu roles and aria-haspopup (except mobile modal)353 - Ensure proper focus management for mobile modal354 - Test with screen readers for proper announcement355 - Maintain clear visual indicators for active states356 - Consider implementing skip links for large navigation357 - Ensure sufficient color contrast for all navigation elements358 - Test keyboard navigation flow thoroughly359360 **Testing Requirements:**361 - Test keyboard navigation through all navigation items362 - Verify dropdown opens/closes with keyboard and mouse363 - Test mobile modal focus management364 - Verify screen reader announcement of navigation structure365 - Test aria-current updates correctly366 - Ensure escape key closes dropdowns and modals367 - Test hover interactions work as expected368 - Verify no auto-opening on keyboard focus369370metadata:371 priority: high372 version: 1.0373</rule>374description:375globs:376alwaysApply: false377---378description:379globs:380alwaysApply: false381---382
Also in Shopify/horizon
Diff this repo’s formatsOne repository carrying more than one format is the comparison this product exists for: does anyone actually write different content in each file, or is one a copy of the other?
| Repository | Format | Stack | Covers | Score | Changed |
|---|---|---|---|---|---|
| Shopify/horizon.cursor/rules/accordion-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/animation-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/blocks.mdc · 428 | Cursor rules | stylearchtypesdatabase+2 | 62/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/breadcrumb-accessibility.mdc · 428 | Cursor rules | ui | 36/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/carousel-accessibility.mdc · 428 | Cursor rules | ui | 32/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/cart-drawer-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/chat-window-accessibility.mdc · 428 | Cursor rules | styleui | 24/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/color-contrast-accessibility.mdc · 428 | Cursor rules | lint-formatui | 44/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/color-swatch-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/commit-messages.mdc · 428 | Cursor rules | setuplint-formattypesgit | 62/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/css-standards.mdc · 428 | Cursor rules | stylearchuiperformance+3 | 49/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/disclosure-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/flip-card-accessibility.mdc · 428 | Cursor rules | styleui | 36/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/form-accessibility.mdc · 428 | Cursor rules | ui | 32/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/javascript-standards.mdc · 428 | Cursor rules | stylearchtypesui+2 | 54/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/landmark-accessibility.mdc · 428 | Cursor rules | ui | 40/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/liquid.mdc · 428 | Cursor rules | buildstyletypesdatabase+2 | 77/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/locales.mdc · 428 | Cursor rules | arch | 49/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/localization.mdc · 428 | Cursor rules | style | 54/100 | 3 days ago | |
| Shopify/horizon.cursor/rules/mobile-accessibility-standards.mdc · 428 | Cursor rules | styleui | 36/100 | 3 days ago |
Diff against .cursor/rules/accordion-accessibility.mdc Diff against .cursor/rules/animation-accessibility.mdc Diff against .cursor/rules/blocks.mdc Diff against .cursor/rules/breadcrumb-accessibility.mdc Diff against .cursor/rules/carousel-accessibility.mdc Diff against .cursor/rules/cart-drawer-accessibility.mdc Diff against .cursor/rules/chat-window-accessibility.mdc Diff against .cursor/rules/color-contrast-accessibility.mdc Diff against .cursor/rules/color-swatch-accessibility.mdc Diff against .cursor/rules/commit-messages.mdc Diff against .cursor/rules/css-standards.mdc Diff against .cursor/rules/disclosure-accessibility.mdc Diff against .cursor/rules/flip-card-accessibility.mdc Diff against .cursor/rules/form-accessibility.mdc Diff against .cursor/rules/javascript-standards.mdc Diff against .cursor/rules/landmark-accessibility.mdc Diff against .cursor/rules/liquid.mdc Diff against .cursor/rules/locales.mdc Diff against .cursor/rules/localization.mdc Diff against .cursor/rules/mobile-accessibility-standards.mdc
