RuleStack

Configs

Stacks

Compare

Diff

RuleStack

Configs

Stacks

Compare

Diff

Read API

RuleStack

Configs

Stacks

Compare

Diff

Read API

Configs/Cursor rules/Shopify/horizon

Cursor rule

.cursor/rules/product-filter-accessibility.mdc

Product filter component accessibility compliance pattern

Cursor rules

Quality

32/100

Scores the file, not the repository.

Length

1,383 words

1 headings · 7 code blocks

Repository

428

— · pushed 17 days ago

Last changed

3 days ago

First indexed 3 days ago.
Shopify/horizon/.cursor/rules/product-filter-accessibility.mdcRawGitHub
1---
2description: Product filter component accessibility compliance pattern
3globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
4alwaysApply: false
5---
6 
7# Product Filter Component Accessibility Standards
8 
9Ensures product filter components follow WCAG compliance and WAI-ARIA Disclosure Pattern specifications, including sort controls and grid layout buttons.
10 
11<rule>
12name: product_filter_accessibility_standards
13description: Enforce product filter component accessibility standards and WAI-ARIA Disclosure Pattern compliance
14filters:
15 - type: file_extension
16 pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
17 
18actions:
19 - type: enforce
20 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."
25 
26 # Filter disclosure aria-expanded requirement
27 - 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'."
30 
31 # Filter disclosure missing keyboard event handlers
32 - 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)."
35 
36 # Missing Escape key support for filter content
37 - 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."
40 
41 # Filter disclosure content not a sibling
42 - 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."
45 
46 # Grid layout buttons missing aria-current
47 - 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'."
50 
51 # Sort filter missing proper labeling
52 - 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."
55 
56 # Sort filter using checkboxes instead of radio buttons
57 - 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."
59 
60 # Checkbox groups missing fieldset
61 - 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."
64 
65 # Fieldset missing legend
66 - pattern: "(?i)<fieldset[^>]*(?:filter|option)[^>]*>"
67 pattern_negate: "<legend"
68 message: "Filter fieldsets must have legend elements to provide context for the group."
69 
70 # Filter options missing proper IDs
71 - pattern: "(?i)<input[^>]*type=\"checkbox\"[^>]*(?:filter|option)[^>]*>"
72 pattern_negate: "id=\"[^\"]+\""
73 message: "Filter checkboxes should have unique ID attributes for proper labeling."
74 
75 # Missing product count live region
76 - pattern: "(?i)<[^>]*(?:product.*count|count.*product)[^>]*>"
77 pattern_negate: "role=\"status\""
78 message: "Product count displays should use role='status' for screen reader announcements."
79 
80 
81 
82 # Missing main products heading
83 - 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."
86 
87 - type: suggest
88 message: |
89 **Product Filter Component Accessibility Best Practices:**
90 
91 **Page Structure Requirements:**
92 - Use `<h1>` for the main "Products" heading
93 - Wrap filter controls and product count in a `<div class="products-header">`
94 - Remove separate section headings for filters and product cards
95 - Present filters, count, and products as one cohesive section
96 
97 **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/removed
101 - Ensure count is announced to screen readers when it changes
102 
103 **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 hidden
106 - **aria-controls:** Reference to the ID of the associated filter content
107 - **aria-current:** Set on grid layout buttons ('true' for active, 'false' for inactive)
108 - **aria-label/aria-labelledby:** Provide context for sort controls
109 
110 **DOM Structure Requirements:**
111 - Filter disclosure content MUST be a sibling to the disclosure control
112 - Checkbox groups should be wrapped in fieldset with legend
113 - Grid layout buttons should be grouped together
114 - Maintain logical reading order in the DOM
115 
116 **Keyboard Interaction Requirements:**
117 - **Enter/Space:** Toggle filter disclosure content visibility
118 - **Tab:** Navigate through filter controls and options
119 - **Arrow keys:** Navigate within checkbox groups
120 - **Escape:** Close open filter disclosures and return focus to launcher button
121 
122 **Implementation Patterns:**
123 
124 **Complete Page Structure:**
125```html
126 <h1>Products</h1>
127 
128 <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>
134 
135 <div role="status" class="product-count">
136 <span id="product-count-text">3 products</span>
137 </div>
138 </div>
139 
140 <div class="product-grid" id="product-grid">
141 <!-- Product cards here -->
142 </div>
143```
144 
145 **Basic Filter Disclosure:**
146```html
147 <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 Filter
153 </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 Small
160 </label>
161 <label for="size-m">
162 <input type="checkbox" id="size-m" name="size" value="m">
163 Medium
164 </label>
165 <label for="size-l">
166 <input type="checkbox" id="size-l" name="size" value="l">
167 Large
168 </label>
169 </fieldset>
170 </div>
171 </div>
172 </form>
173```
174 
175 **Sort Filter with Disclosure:**
176```html
177 <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 by
183 </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 Featured
190 </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```
212 
213 **Grid Layout Controls:**
214```html
215 <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```
232 
233 **JavaScript for Grid Layout:**
234```javascript
235 function setLayout(layout) {
236 const buttons = document.querySelectorAll('[aria-label*="layout"]');
237 
238 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 });
245 
246 // Update visual layout
247 updateProductLayout(layout);
248 }
249```
250 
251 **JavaScript for Filter Toggle with Escape Support:**
252```javascript
253 function toggleFilter(button) {
254 const isExpanded = button.getAttribute('aria-expanded') === 'true';
255 const content = document.getElementById(button.getAttribute('aria-controls'));
256 
257 button.setAttribute('aria-expanded', !isExpanded);
258 content.hidden = isExpanded;
259 
260 if (!isExpanded) {
261 // Add escape key listener to content
262 content.addEventListener('keydown', handleEscapeKey);
263 } else {
264 // Remove escape key listener
265 content.removeEventListener('keydown', handleEscapeKey);
266 }
267 }
268 
269 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 launcher
278 content.removeEventListener('keydown', handleEscapeKey);
279 }
280 }
281 }
282 }
283 
284 // Update product count
285 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 }
291 
292 // Initialize product count on page load
293 document.addEventListener('DOMContentLoaded', function() {
294 updateProductCount();
295 });
296```
297 
298 **Complete Filter Implementation:**
299```html
300 <h1>Products</h1>
301 
302 <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 Size
311 </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 Small
318 </label>
319 <label for="size-m">
320 <input type="checkbox" id="size-m" name="size" value="m">
321 Medium
322 </label>
323 <label for="size-l">
324 <input type="checkbox" id="size-l" name="size" value="l">
325 Large
326 </label>
327 </fieldset>
328 </div>
329 </div>
330 
331 <!-- Color Filter -->
332 <div class="filter-group">
333 <button type="button"
334 aria-expanded="false"
335 aria-controls="color-filter-content">
336 Color
337 </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 Red
344 </label>
345 <label for="color-blue">
346 <input type="checkbox" id="color-blue" name="color" value="blue">
347 Blue
348 </label>
349 <label for="color-green">
350 <input type="checkbox" id="color-green" name="color" value="green">
351 Green
352 </label>
353 </fieldset>
354 </div>
355 </div>
356 
357 <!-- Sort Filter -->
358 <div class="sort-filter">
359 <button type="button"
360 aria-expanded="false"
361 aria-controls="sort-options-content">
362 Sort by
363 </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 Featured
370 </label>
371 <label for="sort-date">
372 <input type="radio" id="sort-date" name="sort" value="date">
373 Date
374 </label>
375 <label for="sort-alphabetical">
376 <input type="radio" id="sort-alphabetical" name="sort" value="alphabetical">
377 Alphabetical
378 </label>
379 </fieldset>
380 </div>
381 </div>
382 
383 <!-- 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>
402 
403 <div role="status" class="product-count">
404 <span id="product-count-text">3 products</span>
405 </div>
406 </div>
407 
408 <div class="product-grid" id="product-grid">
409 <!-- Product cards here -->
410 </div>
411```
412 
413 **JavaScript Considerations:**
414 - Implement Enter and Space key handlers for disclosure toggles
415 - Update aria-expanded state when filter content toggles
416 - Manage aria-current state for layout buttons
417 - Handle filter state changes and update product display
418 - Implement proper focus management when filters open/close
419 - Consider implementing live regions for dynamic content updates
420 
421 **Accessibility Notes:**
422 - Filter content MUST be a sibling to the control in the DOM
423 - Use fieldset and legend for checkbox/radio groups
424 - Provide clear labels for all filter controls
425 - Test with screen readers to ensure proper announcement
426 - Consider adding aria-live regions for dynamic filter results
427 - Maintain proper focus management when filters toggle
428 - Ensure grid layout buttons have clear visual and screen reader indicators
429 
430 **Testing Requirements:**
431 - Test keyboard navigation through all filter controls
432 - Verify disclosure content opens/closes with keyboard
433 - Test screen reader announcement of filter states
434 - Verify aria-current updates correctly on layout changes
435 - Test focus management when filters open/close
436 - Ensure filter results update properly with screen readers
437 
438metadata:
439 priority: high
440 version: 1.0
441</rule>
442description:
443globs:
444alwaysApply: false
445---
446 

Sections

  • Product Filter Component Accessibility Standards

What it covers

ui

Glob targeting

  • *.vue
  • *.jsx
  • *.tsx
  • *.html
  • *.php
  • *.js
  • *.ts
  • *.liquid

Format

Cursor rules

The most expressive format here. Many small .mdc files, each with frontmatter declaring when it should load, so a rule about migrations only enters context when a migration is open. Costs the most to maintain and only one editor reads it.

What the corpus says about it

Repository

Owner
Shopify
Language
—
License
—
Archived
no

All configs in this repo

Also in Shopify/horizon

Diff this repo’s formats

One 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?

The other instruction files in this repository
RepositoryFormatStackCoversScoreChanged
Shopify/horizon.cursor/rules/accordion-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/animation-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/blocks.mdc · 428Cursor rulesunclassifiedstylearchtypesdatabase+262/1003 days ago
Shopify/horizon.cursor/rules/breadcrumb-accessibility.mdc · 428Cursor rulesunclassifiedui36/1003 days ago
Shopify/horizon.cursor/rules/carousel-accessibility.mdc · 428Cursor rulesunclassifiedui32/1003 days ago
Shopify/horizon.cursor/rules/cart-drawer-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/chat-window-accessibility.mdc · 428Cursor rulesunclassifiedstyleui24/1003 days ago
Shopify/horizon.cursor/rules/color-contrast-accessibility.mdc · 428Cursor rulesunclassifiedlint-formatui44/1003 days ago
Shopify/horizon.cursor/rules/color-swatch-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/commit-messages.mdc · 428Cursor rulesunclassifiedsetuplint-formattypesgit62/1003 days ago
Shopify/horizon.cursor/rules/css-standards.mdc · 428Cursor rulesunclassifiedstylearchuiperformance+349/1003 days ago
Shopify/horizon.cursor/rules/disclosure-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/dropdown-navigation-accessibility.mdc · 428Cursor rulesunclassifiedstyleui36/1003 days ago
Shopify/horizon.cursor/rules/flip-card-accessibility.mdc · 428Cursor rulesunclassifiedstyleui36/1003 days ago
Shopify/horizon.cursor/rules/form-accessibility.mdc · 428Cursor rulesunclassifiedui32/1003 days ago
Shopify/horizon.cursor/rules/javascript-standards.mdc · 428Cursor rulesunclassifiedstylearchtypesui+254/1003 days ago
Shopify/horizon.cursor/rules/landmark-accessibility.mdc · 428Cursor rulesunclassifiedui40/1003 days ago
Shopify/horizon.cursor/rules/liquid.mdc · 428Cursor rulesunclassifiedbuildstyletypesdatabase+277/1003 days ago
Shopify/horizon.cursor/rules/locales.mdc · 428Cursor rulesunclassifiedarch49/1003 days ago
Shopify/horizon.cursor/rules/localization.mdc · 428Cursor rulesunclassifiedstyle54/1003 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
RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack

RuleStack

Built by

Kynth Studio

Directory

Configs
Stacks
Compare formats
Diff two configs
Best AGENTS.md examples

Formats

AGENTS.md
CLAUDE.md
Cursor rules
Copilot instructions

Reference

Read API
Corpus health
Privacy Policy
Terms

RuleStack