Cursor rule
.cursor/rules/product-filter-accessibility.mdcProduct filter component accessibility compliance pattern
Cursor rules
Quality
32/100
Scores the file, not the repository.Length
1,383 words
1 headings · 7 code blocksRepository
428
— · pushed 17 days agoLast changed
3 days ago
First indexed 3 days ago.1234567# Product Filter Component Accessibility Standards89Ensures product filter components follow WCAG compliance and WAI-ARIA Disclosure Pattern specifications, including sort controls and grid layout buttons.1011<rule>12name: product_filter_accessibility_standards13description: Enforce product filter component accessibility standards and WAI-ARIA Disclosure Pattern compliance14filters:15 - type: file_extension16 pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"1718actions:19 - type: enforce20 conditions:21 # Filter disclosure button role requirement (for non-button elements)22 - pattern: "(?i)<(div|span)[^>]*(?:filter|disclosure|expand|collapse)[^>]*>"23 pattern_negate: "role=\"button\""24 message: "Non-button filter disclosure controls must have role='button'. Native button elements have implicit role and don't need explicit role attribute."2526 # Filter disclosure aria-expanded requirement27 - pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:filter|disclosure|expand|collapse)[^>]*>"28 pattern_negate: "aria-expanded=\"(true|false)\""29 message: "Filter disclosure controls must have aria-expanded attribute set to 'true' or 'false'."3031 # Filter disclosure missing keyboard event handlers32 - pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:filter|disclosure|expand|collapse)[^>]*>"33 pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"34 message: "Filter disclosure controls should handle keyboard events (Enter, Space, and Escape)."3536 # Missing Escape key support for filter content37 - pattern: "(?i)<div[^>]*(?:filter.*content|content.*filter)[^>]*>"38 pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"39 message: "Filter content areas should handle Escape key to close filter and return focus to launcher."4041 # Filter disclosure content not a sibling42 - pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:filter|disclosure)[^>]*>"43 pattern_negate: "<(div|section)[^>]*id=\"[^\"]+\"[^>]*>"44 message: "Filter disclosure content must be a sibling to the disclosure control in the DOM."4546 # Grid layout buttons missing aria-current47 - pattern: "(?i)<(button|div|span)[^>]*(?:grid|layout|view)[^>]*>"48 pattern_negate: "aria-current=\"(true|false)\""49 message: "Grid layout buttons must have aria-current attribute set to 'true' or 'false'."5051 # Sort filter missing proper labeling52 - pattern: "(?i)<(button|div|span)[^>]*(?:sort|order)[^>]*>"53 pattern_negate: "(aria-label|aria-labelledby)"54 message: "Sort filter controls should have proper labeling for screen reader context."5556 # Sort filter using checkboxes instead of radio buttons57 - pattern: "(?i)<input[^>]*type=\"checkbox\"[^>]*(?:sort|order)[^>]*>"58 message: "Sort filter options should use radio buttons since only one option can be selected at a time."5960 # Checkbox groups missing fieldset61 - pattern: "(?i)<input[^>]*type=\"checkbox\"[^>]*(?:filter|option)[^>]*>"62 pattern_negate: "<fieldset"63 message: "Filter checkbox groups should be wrapped in fieldset elements for proper grouping."6465 # Fieldset missing legend66 - pattern: "(?i)<fieldset[^>]*(?:filter|option)[^>]*>"67 pattern_negate: "<legend"68 message: "Filter fieldsets must have legend elements to provide context for the group."6970 # Filter options missing proper IDs71 - pattern: "(?i)<input[^>]*type=\"checkbox\"[^>]*(?:filter|option)[^>]*>"72 pattern_negate: "id=\"[^\"]+\""73 message: "Filter checkboxes should have unique ID attributes for proper labeling."7475 # Missing product count live region76 - pattern: "(?i)<[^>]*(?:product.*count|count.*product)[^>]*>"77 pattern_negate: "role=\"status\""78 message: "Product count displays should use role='status' for screen reader announcements."79808182 # Missing main products heading83 - pattern: "(?i)<[^>]*(?:product.*filter|filter.*product)[^>]*>"84 pattern_negate: "<h1[^>]*>.*[Pp]roducts?[^<]*</h1>"85 message: "Product filter pages should have an h1 heading with 'Products' for proper page structure."8687 - type: suggest88 message: |89 **Product Filter Component Accessibility Best Practices:**9091 **Page Structure Requirements:**92 - Use `<h1>` for the main "Products" heading93 - Wrap filter controls and product count in a `<div class="products-header">`94 - Remove separate section headings for filters and product cards95 - Present filters, count, and products as one cohesive section9697 **Product Count Live Region:**98 - Add product count display with `role="status"`99 - Use unique ID for the count text element (e.g., `id="product-count-text"`)100 - Update count dynamically as filters are applied/removed101 - Ensure count is announced to screen readers when it changes102103 **Required ARIA Attributes:**104 - **role='button':** Only required for non-button elements (native button elements have implicit role)105 - **aria-expanded:** 'true' if filter content is visible, 'false' if hidden106 - **aria-controls:** Reference to the ID of the associated filter content107 - **aria-current:** Set on grid layout buttons ('true' for active, 'false' for inactive)108 - **aria-label/aria-labelledby:** Provide context for sort controls109110 **DOM Structure Requirements:**111 - Filter disclosure content MUST be a sibling to the disclosure control112 - Checkbox groups should be wrapped in fieldset with legend113 - Grid layout buttons should be grouped together114 - Maintain logical reading order in the DOM115116 **Keyboard Interaction Requirements:**117 - **Enter/Space:** Toggle filter disclosure content visibility118 - **Tab:** Navigate through filter controls and options119 - **Arrow keys:** Navigate within checkbox groups120 - **Escape:** Close open filter disclosures and return focus to launcher button121122 **Implementation Patterns:**123124 **Complete Page Structure:**125```html126 <h1>Products</h1>127128 <div class="products-header">129 <form action="/products/filter" method="get" id="product-filters-form">130 <div class="filter-section">131 <!-- Filter groups here -->132 </div>133 </form>134135 <div role="status" class="product-count">136 <span id="product-count-text">3 products</span>137 </div>138 </div>139140 <div class="product-grid" id="product-grid">141 <!-- Product cards here -->142 </div>143```144145 **Basic Filter Disclosure:**146```html147 <form action="/products/filter" method="get" id="product-filters-form">148 <div class="filter-group">149 <button type="button"150 aria-expanded="false"151 aria-controls="size-filter-content">152 Size Filter153 </button>154 <div id="size-filter-content" hidden>155 <fieldset>156 <legend>Select sizes</legend>157 <label for="size-s">158 <input type="checkbox" id="size-s" name="size" value="s">159 Small160 </label>161 <label for="size-m">162 <input type="checkbox" id="size-m" name="size" value="m">163 Medium164 </label>165 <label for="size-l">166 <input type="checkbox" id="size-l" name="size" value="l">167 Large168 </label>169 </fieldset>170 </div>171 </div>172 </form>173```174175 **Sort Filter with Disclosure:**176```html177 <form action="/products/filter" method="get" id="product-filters-form">178 <div class="sort-filter">179 <button type="button"180 aria-expanded="false"181 aria-controls="sort-options-content">182 Sort by183 </button>184 <div id="sort-options-content" hidden>185 <fieldset>186 <legend>Sort options</legend>187 <label for="sort-featured">188 <input type="radio" id="sort-featured" name="sort" value="featured">189 Featured190 </label>191 <label for="sort-date-newest">192 <input type="radio" id="sort-date-newest" name="sort" value="date-newest">193 Date (Newest to Oldest)194 </label>195 <label for="sort-date-oldest">196 <input type="radio" id="sort-date-oldest" name="sort" value="date-oldest">197 Date (Oldest to Newest)198 </label>199 <label for="sort-alphabetical-az">200 <input type="radio" id="sort-alphabetical-az" name="sort" value="alphabetical-az">201 Alphabetical (A-Z)202 </label>203 <label for="sort-alphabetical-za">204 <input type="radio" id="sort-alphabetical-za" name="sort" value="alphabetical-za">205 Alphabetical (Z-A)206 </label>207 </fieldset>208 </div>209 </div>210 </form>211```212213 **Grid Layout Controls:**214```html215 <div class="layout-controls">216 <button type="button"217 aria-current="true"218 aria-label="Grid layout"219 onclick="setLayout('grid')">220 <span class="sr-only">Grid layout</span>221 <svg><!-- Grid icon --></svg>222 </button>223 <button type="button"224 aria-current="false"225 aria-label="List layout"226 onclick="setLayout('list')">227 <span class="sr-only">List layout</span>228 <svg><!-- List icon --></svg>229 </button>230 </div>231```232233 **JavaScript for Grid Layout:**234```javascript235 function setLayout(layout) {236 const buttons = document.querySelectorAll('[aria-label*="layout"]');237238 buttons.forEach(button => {239 if (button.getAttribute('aria-label').includes(layout)) {240 button.setAttribute('aria-current', 'true');241 } else {242 button.setAttribute('aria-current', 'false');243 }244 });245246 // Update visual layout247 updateProductLayout(layout);248 }249```250251 **JavaScript for Filter Toggle with Escape Support:**252```javascript253 function toggleFilter(button) {254 const isExpanded = button.getAttribute('aria-expanded') === 'true';255 const content = document.getElementById(button.getAttribute('aria-controls'));256257 button.setAttribute('aria-expanded', !isExpanded);258 content.hidden = isExpanded;259260 if (!isExpanded) {261 // Add escape key listener to content262 content.addEventListener('keydown', handleEscapeKey);263 } else {264 // Remove escape key listener265 content.removeEventListener('keydown', handleEscapeKey);266 }267 }268269 function handleEscapeKey(event) {270 if (event.key === 'Escape') {271 const content = event.target.closest('[hidden]');272 if (content) {273 const button = document.querySelector(`[aria-controls="${content.id}"]`);274 if (button) {275 button.setAttribute('aria-expanded', 'false');276 content.hidden = true;277 button.focus(); // Return focus to launcher278 content.removeEventListener('keydown', handleEscapeKey);279 }280 }281 }282 }283284 // Update product count285 function updateProductCount() {286 const visibleProducts = document.querySelectorAll('.product-card:not([hidden])');287 const count = visibleProducts.length;288 const countText = document.getElementById('product-count-text');289 countText.textContent = `${count} product${count !== 1 ? 's' : ''}`;290 }291292 // Initialize product count on page load293 document.addEventListener('DOMContentLoaded', function() {294 updateProductCount();295 });296```297298 **Complete Filter Implementation:**299```html300 <h1>Products</h1>301302 <div class="products-header">303 <form action="/products/filter" method="get" id="product-filters-form">304 <div class="filter-section">305 <!-- Size Filter -->306 <div class="filter-group">307 <button type="button"308 aria-expanded="false"309 aria-controls="size-filter-content">310 Size311 </button>312 <div id="size-filter-content" hidden>313 <fieldset>314 <legend>Select sizes</legend>315 <label for="size-s">316 <input type="checkbox" id="size-s" name="size" value="s">317 Small318 </label>319 <label for="size-m">320 <input type="checkbox" id="size-m" name="size" value="m">321 Medium322 </label>323 <label for="size-l">324 <input type="checkbox" id="size-l" name="size" value="l">325 Large326 </label>327 </fieldset>328 </div>329 </div>330331 <!-- Color Filter -->332 <div class="filter-group">333 <button type="button"334 aria-expanded="false"335 aria-controls="color-filter-content">336 Color337 </button>338 <div id="color-filter-content" hidden>339 <fieldset>340 <legend>Select colors</legend>341 <label for="color-red">342 <input type="checkbox" id="color-red" name="color" value="red">343 Red344 </label>345 <label for="color-blue">346 <input type="checkbox" id="color-blue" name="color" value="blue">347 Blue348 </label>349 <label for="color-green">350 <input type="checkbox" id="color-green" name="color" value="green">351 Green352 </label>353 </fieldset>354 </div>355 </div>356357 <!-- Sort Filter -->358 <div class="sort-filter">359 <button type="button"360 aria-expanded="false"361 aria-controls="sort-options-content">362 Sort by363 </button>364 <div id="sort-options-content" hidden>365 <fieldset>366 <legend>Sort options</legend>367 <label for="sort-featured">368 <input type="radio" id="sort-featured" name="sort" value="featured">369 Featured370 </label>371 <label for="sort-date">372 <input type="radio" id="sort-date" name="sort" value="date">373 Date374 </label>375 <label for="sort-alphabetical">376 <input type="radio" id="sort-alphabetical" name="sort" value="alphabetical">377 Alphabetical378 </label>379 </fieldset>380 </div>381 </div>382383 <!-- Layout Controls -->384 <div class="layout-controls">385 <button type="button"386 aria-current="true"387 aria-label="Grid layout"388 onclick="setLayout('grid')">389 <span class="sr-only">Grid layout</span>390 <svg><!-- Grid icon --></svg>391 </button>392 <button type="button"393 aria-current="false"394 aria-label="List layout"395 onclick="setLayout('list')">396 <span class="sr-only">List layout</span>397 <svg><!-- List icon --></svg>398 </button>399 </div>400 </div>401 </form>402403 <div role="status" class="product-count">404 <span id="product-count-text">3 products</span>405 </div>406 </div>407408 <div class="product-grid" id="product-grid">409 <!-- Product cards here -->410 </div>411```412413 **JavaScript Considerations:**414 - Implement Enter and Space key handlers for disclosure toggles415 - Update aria-expanded state when filter content toggles416 - Manage aria-current state for layout buttons417 - Handle filter state changes and update product display418 - Implement proper focus management when filters open/close419 - Consider implementing live regions for dynamic content updates420421 **Accessibility Notes:**422 - Filter content MUST be a sibling to the control in the DOM423 - Use fieldset and legend for checkbox/radio groups424 - Provide clear labels for all filter controls425 - Test with screen readers to ensure proper announcement426 - Consider adding aria-live regions for dynamic filter results427 - Maintain proper focus management when filters toggle428 - Ensure grid layout buttons have clear visual and screen reader indicators429430 **Testing Requirements:**431 - Test keyboard navigation through all filter controls432 - Verify disclosure content opens/closes with keyboard433 - Test screen reader announcement of filter states434 - Verify aria-current updates correctly on layout changes435 - Test focus management when filters open/close436 - Ensure filter results update properly with screen readers437438metadata:439 priority: high440 version: 1.0441</rule>442description:443globs:444alwaysApply: false445---446
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/dropdown-navigation-accessibility.mdc · 428 | Cursor rules | styleui | 36/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 |
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/dropdown-navigation-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
