```
**9. Animation State Management:**
```css
/* Animation states */
.animated-element {
opacity: 0;
transform: translateY(20px);
transition: all 0.3s ease-out;
}
.animated-element.animate {
opacity: 1;
transform: translateY(0);
}
.animated-element.paused {
animation-play-state: paused;
}
/* Reduced motion states */
@media (prefers-reduced-motion: reduce) {
.animated-element {
opacity: 1;
transform: none;
transition: none;
}
}
```
**10. Testing and Validation:**
```javascript
// Test animation accessibility
function testAnimationAccessibility() {
const issues = [];
// Check for flashing animations
const flashingElements = document.querySelectorAll('[class*="flash"], [class*="pulse"]');
flashingElements.forEach(element => {
const style = window.getComputedStyle(element);
const duration = parseFloat(style.animationDuration);
if (duration < 0.33) { // Below 3Hz threshold
issues.push(`Flashing animation too fast: ${element.className}`);
}
});
// Check for motion preference support
const hasReducedMotion = document.querySelector('@media (prefers-reduced-motion: reduce)');
if (!hasReducedMotion) {
issues.push('Missing reduced motion alternatives');
}
return issues;
}
```
**Animation Guidelines:**
**Keep These Animations:**
- **Fading:** Safe default for most transitions
- **Loading indicators:** Essential for user understanding
- **Subtle scaling:** Gentle size changes for feedback
- **Color transitions:** Slow, smooth color changes
**Subdue These Animations:**
- **System-triggered:** Make unexpected animations subtle
- **Large movements:** Reduce distance and speed
- **Color changes:** Use gentler transitions
**Remove These Animations:**
- **Decorative effects:** Remove purely visual animations
- **Spatial movement:** Replace with gentle fades
- **Auto-playing loops:** Remove or provide pause controls
**Testing Checklist:**
- Test with prefers-reduced-motion: reduce
- Verify animations don't exceed 3Hz frequency
- Check that essential UI changes are clear without animation
- Ensure pause controls work for looping animations
- Test with screen readers and assistive technology
- Validate motion doesn't cause disorientation
**Common Mistakes to Avoid:**
- Missing prefers-reduced-motion media queries
- Flashing animations above 3Hz threshold
- Large spatial movements without alternatives
- Auto-playing animations without pause controls
- Parallax effects that ignore user preferences
- Animations that patch design shortcomings
- Missing focus indicators on animated elements
- Excessive animation duration causing motion sickness
metadata:
priority: high
version: 1.0
description:
globs:
alwaysApply: false
---
================================================
FILE: .cursor/rules/assets.mdc
================================================
---
description: Static files (css, js, and images) for theme templates
globs: assets/*
alwaysApply: false
---
# Assets
The assets directory contains any assets that need to be referenced within a `.liquid` file, usually using the [asset_url](mdc:https:/shopify.dev/docs/api/liquid/filters/asset_url) Liquid filter.
Assets is a flat directory, it may not contain subdirectories.
Any images that are required in the code, including icons, may be stored within assets. Icons can be used in `.liquid` files via the [inline_asset_content](mdc:https:/shopify.dev/docs/api/liquid/filters/inline_asset_content) Liquid filter.
================================================
FILE: .cursor/rules/blocks.mdc
================================================
---
description: Development standards and best practices for creating/configuring/styling theme blocks, including static and nested blocks, schema configuration, CSS, and usage examples
globs: blocks/*.liquid
alwaysApply: false
---
# Theme Blocks Development Standards
Follow [Shopify's theme blocks documentation](mdc:https:/shopify.dev/docs/storefronts/themes/architecture/blocks/theme-blocks/quick-start?framework=liquid.txt).
## Theme Block Fundamentals
Theme blocks are reusable components defined at the theme level that can be:
- Nested under sections and blocks
- Configured using settings in the theme editor
- Given presets and added by merchants
- Used as [static blocks](mdc:https:/shopify.dev/docs/storefronts/themes/architecture/blocks/theme-blocks/static-blocks#statically-vs-dynamically-rendered-theme-blocks) by theme developers
Blocks render in the editor and storefront when they are referenced in [template files](mdc:.cursor/rules/templates.mdc).
### Basic Block Structure
```liquid
{% doc %}
Block description and usage examples
@example
{% content_for 'block', type: 'block-name', id: 'unique-id' %}
{% enddoc %}
{% stylesheet %}
/*
Scoped CSS for this block
Use BEM structure
CSS written in here should be for components that are exclusively in this block. If the CSS will be used elsewhere, it should instead be written in [assets/base.css](mdc:@assets/base.css)
*/
{% endstylesheet %}
{% schema %}
{
"name": "Block Name",
"settings": [],
"presets": []
}
{% endschema %}
```
### Static Block Usage
Static blocks are theme blocks that are rendered directly in Liquid templates by developers, rather than being dynamically added through the theme editor. This allows for predetermined block placement with optional default settings.
**Basic Static Block Syntax:**
```liquid
{% content_for 'block', type: 'text', id: 'header-announcement' %}
```
**Example: Product Template with Mixed Static and Dynamic Blocks**
```liquid
```
**Key Points about Static Blocks:**
- They have a fixed `id` that makes them identifiable in the theme editor
- Settings can be overridden in the theme editor despite having defaults
- They appear in the theme editor as locked blocks that can't be removed or reordered
- Useful for consistent layout elements that should always be present
- Can be mixed with dynamic block areas using `{% content_for 'blocks' %}`
## Schema Configuration
See [schemas.mdc](mdc:.cursor/rules/schemas.mdc) for rules on schemas
### Advanced Schema Features
#### Exclude wrapper
```json
{
"tag": null // No wrapper - must include {{ block.shopify_attributes }} for proper editor function
}
```
## Block Implementation Patterns
### Accessing Block Data
**Block Settings:**
```liquid
{{ block.settings.text }}
{{ block.settings.heading | escape }}
{{ block.settings.image | image_url: width: 800 }}
```
**Block Properties:**
```liquid
{{ block.id }} // Unique block identifier {{ block.type }} // Block type name {{ block.shopify_attributes }} // Required
for theme editor
```
**Section Context:**
```liquid
{{ section.id }} // Parent section ID
{{ section.settings.heading | escape }}
{{ section.settings.image | image_url: width: 800 }}
```
## Nested Blocks Implementation
### Critical Constraint: Single `content_for 'blocks'` Per File
**IMPORTANT:** There can only be **ONE** `{% content_for 'blocks' %}` call per Liquid file. If you need to use the blocks content in multiple places (e.g., in conditional branches), you must capture it first:
```liquid
{% comment %} ✅ CORRECT - Capture once, use multiple times {% endcomment %}
{% capture blocks_content %}
{% content_for 'blocks' %}
{% endcapture %}
{% if condition %}
{{ blocks_content }}
{% else %}
{{ blocks_content }}
{% endif %}
```
```liquid
{% comment %} ❌ INCORRECT - Multiple content_for calls will cause errors {% endcomment %}
{% if condition %}
```
## Performance Best Practices
### Conditional Rendering
```liquid
{% liquid
assign has_content = false
if block.settings.heading != blank or block.settings.text != blank
assign has_content = true
endif
%}
{% if has_content %}
{% endif %}
```
## Examples Referenced
[text.liquid](mdc:.cursor/rules/examples/block-example-text.liquid) - Basic content block from existing project
[group.liquid](mdc:.cursor/rules/examples/block-example-group.liquid) - Container with nested blocks from existing project
================================================
FILE: .cursor/rules/breadcrumb-accessibility.mdc
================================================
---
description: Breadcrumb component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Breadcrumb Accessibility
Ensures breadcrumb components follow WCAG compliance and WAI-ARIA Breadcrumb Pattern specifications.
name: breadcrumb_accessibility_standards
description: Enforce breadcrumb component accessibility standards and WAI-ARIA Breadcrumb Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Navigation landmark requirement
- pattern: "(?i)
================================================
FILE: .cursor/rules/carousel-accessibility.mdc
================================================
---
description: Carousel component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Carousel Accessibility Standards
Ensures carousel components follow WCAG compliance and WAI-ARIA Carousel Pattern specifications.
name: carousel_accessibility_standards
description: Enforce carousel component accessibility standards and WAI-ARIA Carousel Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Carousel container role requirement
- pattern: "(?i)<(div|section)[^>]*(?:carousel|slider|slideshow)[^>]*>"
pattern_negate: "(role=\"(region|group)\"|aria-roledescription=\"carousel\")"
message: "Carousel container must have role='region' or role='group' and aria-roledescription='carousel'."
# Carousel label requirement
- pattern: "(?i)<[^>]*role=\"(region|group)\"[^>]*aria-roledescription=\"carousel\"[^>]*>"
pattern_negate: "(aria-labelledby|aria-label)=\"[^\"]+\""
message: "Carousel must have either aria-labelledby or aria-label for accessibility."
# Slide role requirement
- pattern: "(?i)<(div|section)[^>]*(?:slide|carousel-item)[^>]*>"
pattern_negate: "(role=\"group\"|aria-roledescription=\"slide\")"
message: "Slide containers must have role='group' and aria-roledescription='slide'."
# Slide label requirement
- pattern: "(?i)<[^>]*role=\"group\"[^>]*aria-roledescription=\"slide\"[^>]*>"
pattern_negate: "(aria-labelledby|aria-label)=\"[^\"]+\""
message: "Slides must have either aria-labelledby or aria-label for accessibility."
# Rotation control requirement
- pattern: "(?i)]*(?:rotation|auto-play|autoplay)[^>]*>"
pattern_negate: "aria-label=\"[^\"]*(?:Start|Stop)[^\"]*slide[^\"]*rotation[^\"]*\""
message: "Rotation control must have aria-label indicating its current state (Start/Stop slide rotation)."
# Navigation controls requirement
- pattern: "(?i)]*(?:next|previous|prev)[^>]*>"
pattern_negate: "aria-label=\"[^\"]*(?:Next|Previous)[^\"]*slide[^\"]*\""
message: "Navigation controls must have aria-label indicating their purpose (Next/Previous slide)."
# Navigation button disabling check
- pattern: "(?i)\\.disabled.*next|previous.*disabled"
message: "Navigation buttons should not be disabled. Implement wrap-around navigation to first/last slide instead for better user experience."
# Missing keyboard event handlers
- pattern: "(?i)]*(?:rotation|next|previous|prev)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Carousel controls should handle keyboard events (Enter, Space)."
# Auto-rotation interval check (WCAG 2.2.2)
- pattern: "setInterval\\([^,]+,\\s*(?:[0-4]\\d{3}|[0-9]{1,4})\\)"
message: "Auto-rotation interval must be at least 5000ms (5 seconds) to comply with WCAG 2.2.2 Pause, Stop, Hide."
# Mouse hover event handlers check
- pattern: "(?i)<(div|section)[^>]*(?:carousel|slider|slideshow)[^>]*>"
pattern_negate: "(onMouseEnter|onmouseenter|@mouseenter|v-on:mouseenter|onMouseLeave|onmouseleave|@mouseleave|v-on:mouseleave)"
message: "Carousel must handle mouseenter/mouseleave events to pause/resume auto-rotation."
# aria-live attribute check
- pattern: "(?i)<[^>]*aria-live=\"[^\"]*\"[^>]*>"
pattern_negate: "aria-live=\"(off|polite)\""
message: "Carousel container must have aria-live set to 'off' during rotation and 'polite' when paused."
- type: suggest
message: |
**Carousel Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **role='region' or role='group':** Set on carousel container
- **aria-roledescription='carousel':** Set on carousel container
- **aria-labelledby/aria-label:** Set on carousel container
- **role='group':** Set on slide containers
- **aria-roledescription='slide':** Set on slide containers
- **aria-labelledby/aria-label:** Set on slide containers
- **aria-label:** Set on rotation control (changes with state)
- **aria-label:** Set on navigation controls
- **aria-live:** Set to 'off' during rotation, 'polite' when paused
**Optional ARIA Attributes:**
- **aria-atomic='false':** On slide wrapper
- **aria-hidden='true':** Set on inactive slides to hide from screen readers
- **visibility: hidden:** CSS property on inactive slides to hide from keyboard and visual users
**Keyboard Interaction Requirements:**
- **Tab/Shift+Tab:** Navigate through interactive elements
- **Enter/Space:** Activate controls
- **Auto-rotation:** Stops on focus or mouse hover, resumes on blur or mouse away
- **Rotation Control:** First in tab sequence
**Navigation Button Best Practices:**
- **Always Enabled:** Navigation buttons should never be disabled
- **Wrap-Around Navigation:** Next button wraps to first slide, Previous button wraps to last slide
- **Consistent Behavior:** Users can always navigate in both directions
- **Better UX:** Prevents users from getting "stuck" at slide boundaries
**Auto-rotation Requirements (WCAG 2.2.2):**
- Minimum interval between slides: 5 seconds
- Must provide pause/stop control
- Must stop on user interaction
- Must stop when any element receives focus
- Must stop when mouse hovers over carousel
- Must resume when mouse leaves carousel (unless manually paused)
- Must not restart automatically after manual pause
**Mouse Interaction Requirements:**
- Pause auto-rotation on mouseenter
- Resume auto-rotation on mouseleave (unless manually paused)
- Maintain pause state when manually stopped
- Clear visual indication of pause state
**Play/Pause Button Requirements:**
- Button state should reflect user's explicit choice
- Button state should not change with temporary auto-rotation pauses
- Button should maintain its state across mouse/focus events
- Button should only change state when explicitly activated
- Button should provide clear visual feedback of current state
- Button state should be independent of focus/hover pause behavior
**State Management Requirements:**
- Track rotation state (isRotating)
- Track manual pause state (wasManuallyPaused)
- Track focus/hover pause state (isPausedByFocus)
- Update aria-live attribute based on rotation state
- Maintain button state across temporary pauses
- Handle state transitions appropriately
**Structure Requirements:**
- Use native button elements for controls
- Handle auto-rotation state changes
- Provide clear visual indicators
- Ensure proper slide labeling
- Hide off-screen slides from keyboard and screen reader users
**Slide Visibility Management:**
- Use `visibility: hidden` on inactive slides to hide from all users
- Use `visibility: visible` on active slide to make accessible
- The `visibility` property can be animated with CSS transitions
- Prevents keyboard focus on hidden slides
- Prevents screen reader access to hidden slides
- Improves performance by removing off-screen content from accessibility tree
**Implementation Patterns:**
**Basic Carousel:**
```html
Pause
Product 1
Previous
Next
```
**Tab Controls Implementation:**
```html
Pause
Product 1
Previous
Next
Slide 1
Slide 2
Slide 3
```
**JavaScript Considerations:**
- Implement auto-rotation pause on focus or mouse hover
- Handle keyboard navigation
- Update ARIA labels for rotation state
- Implement proper tab sequence
- Handle slide picker controls
- Consider touch/swipe interactions
- Ensure minimum 5-second interval between slides
- Stop rotation on user interaction
- Do not restart rotation automatically after manual pause
- Handle mouse enter/leave events for pause/resume
- Maintain manual pause state across mouse events
- Separate auto-rotation pause behavior from Play/Pause button state
- Update button state only on explicit user interaction
- **Update aria-live attribute dynamically based on rotation state**
- Track and manage multiple pause states (manual vs. focus/hover)
**aria-live Management Example:**
```javascript
function toggleRotation() {
const carouselContainer = document.querySelector('[aria-live]');
wasManuallyPaused = isRotating;
isRotating = !isRotating;
if (isRotating) {
startRotation();
playPauseButton.textContent = 'Pause';
playPauseButton.setAttribute('aria-label', 'Stop slide rotation');
// Set to 'off' when rotating - prevents announcement overload
carouselContainer.setAttribute('aria-live', 'off');
} else {
stopRotation();
playPauseButton.textContent = 'Play';
playPauseButton.setAttribute('aria-label', 'Start slide rotation');
// Set to 'polite' when paused - allows screen readers to announce changes
carouselContainer.setAttribute('aria-live', 'polite');
}
}
// Also update aria-live on hover pause/resume
function pauseRotation() {
if (!wasManuallyPaused && isRotating) {
stopRotation();
const carouselContainer = document.querySelector('[aria-live]');
carouselContainer.setAttribute('aria-live', 'polite');
}
}
function resumeRotation() {
if (!wasManuallyPaused && isRotating) {
startRotation();
const carouselContainer = document.querySelector('[aria-live]');
carouselContainer.setAttribute('aria-live', 'off');
}
}
```
**Slide Visibility Management Example:**
```css
/* Hide all slides by default */
.carousel-slide {
visibility: hidden;
opacity: 0;
transition: visibility 0.5s ease, opacity 0.5s ease, transform 0.5s ease;
}
/* Show active slide */
.carousel-slide.active {
visibility: visible;
opacity: 1;
}
/* Alternative: Use data attribute for active state */
.carousel-slide[data-active="false"] {
visibility: hidden;
opacity: 0;
}
.carousel-slide[data-active="true"] {
visibility: visible;
opacity: 1;
}
```
```javascript
function updateSlide(index) {
const slides = document.querySelectorAll('.carousel-slide');
currentSlide = (index + slides.length) % slides.length;
slides.forEach((slide, i) => {
if (i === currentSlide) {
// Show active slide
slide.classList.add('active');
slide.setAttribute('aria-hidden', 'false');
slide.setAttribute('data-active', 'true');
} else {
// Hide inactive slides
slide.classList.remove('active');
slide.setAttribute('aria-hidden', 'true');
slide.setAttribute('data-active', 'false');
}
});
// Update visual position
slidesContainer.style.transform = `translateX(-${currentSlide * 100}%)`;
}
```
**Accessibility Notes:**
- Auto-rotation should be paused by default
- Provide clear visual focus indicators
- Ensure sufficient color contrast
- Test with screen readers
- Consider motion sensitivity
- Provide alternative navigation methods
- Comply with WCAG 2.2.2 Pause, Stop, Hide requirement
- Ensure mouse hover behavior is consistent and predictable
- Maintain clear distinction between temporary and permanent pause states
- Ensure screen reader announcements are appropriate for current state
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/cart-drawer-accessibility.mdc
================================================
---
description: Cart drawer component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Cart Drawer Component Accessibility Standards
Ensures cart drawer components follow WCAG compliance and ARIA Dialog Pattern specifications for ecommerce applications.
name: cart_drawer_accessibility_standards
description: Enforce cart drawer component accessibility standards and ARIA Dialog Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Cart activator missing aria-haspopup
- pattern: "(?i)]*(?:cart|basket|shopping)[^>]*>"
pattern_negate: "aria-haspopup=\"dialog\""
message: "Cart activator buttons must include aria-haspopup='dialog' to inform users a dialog will open."
# Cart container missing dialog role
- pattern: "(?i)<(div|section|aside)[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "role=\"dialog\""
message: "Cart drawer containers must have role='dialog' attribute."
# Cart container missing aria-modal
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "aria-modal=\"true\""
message: "Cart drawer dialog elements must have aria-modal='true' attribute."
# Cart container missing proper labeling
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "(aria-labelledby|aria-label)"
message: "Cart drawer dialog elements must have either aria-labelledby or aria-label for accessibility."
# Empty aria-label check
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:cart|basket|drawer)[^>]*aria-label=\"\"[^>]*>"
message: "Cart drawer aria-label should not be empty; provide a meaningful description like 'Shopping Cart'."
# Close button missing proper functionality
- pattern: "(?i)]*(?:close|dismiss|cancel)[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "(onClick|onclick|@click|v-on:click)"
message: "Cart drawer close buttons should have proper click handlers to close the dialog."
# Close button missing aria-label
- pattern: "(?i)]*(?:close|dismiss|×|×)[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "aria-label=\"[^\"]*[Cc]lose[^\"]*\""
message: "Cart drawer close buttons should have aria-label='Close cart' or similar descriptive text."
# Missing focus management indicators
- pattern: "(?i)(?:openCart|showCart|toggleCart|openDrawer)\\s*\\("
message: "When opening cart drawers, ensure focus management is implemented (focus should move to first focusable element inside the dialog)."
# Missing checkout button accessibility
- pattern: "(?i)]*(?:checkout|proceed|purchase)[^>]*(?:cart|basket|drawer)[^>]*>"
pattern_negate: "(aria-label|aria-describedby)"
message: "Cart drawer checkout buttons should have proper labeling for screen readers."
# Quantity inputs missing aria-live for screen reader announcements
- pattern: "(?i)]*type=\"number\"[^>]*(?:quantity|qty)[^>]*>"
pattern_negate: "aria-live=\"polite\""
message: "Cart quantity inputs must have aria-live='polite' to announce value changes to screen readers."
# Missing focus management for item removal
- pattern: "(?i)(?:removeItem|remove.*item|delete.*item)\\s*\\("
pattern_negate: "focus\\(|focus\\(\\).*close|close.*focus\\(\\"
message: "When removing cart items, implement focus management to shift focus to a logical location (e.g., close button) for better user experience."
- type: suggest
message: |
**Cart Drawer Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **aria-haspopup='dialog':** Set on cart activator buttons to inform users a dialog will open
- **role='dialog':** Set on the cart drawer container element
- **aria-modal='true':** Indicates the cart drawer is modal and traps focus
- **aria-labelledby:** Reference to visible cart title, OR
- **aria-label:** Descriptive label like "Shopping Cart" if no visible title exists
**Keyboard Interaction Requirements:**
- **Initial Focus:** When cart drawer opens, focus must move to the first focusable element (typically close button)
- **Tab Cycling:** Tab key should cycle through focusable elements within the cart drawer only
- **Shift+Tab:** Should cycle backwards through focusable elements within the cart drawer
- **Escape Key:** Must close the cart drawer and return focus to the activator
- **Focus Trap:** Focus should be contained within the cart drawer while open
**Focus Management:**
- Implement focus trapping to prevent tab navigation outside the cart drawer
- Return focus to the cart activator when drawer closes
- Move focus to the close button (first focusable element) when drawer opens
- Ensure close button is positioned first in DOM order within the dialog container
- **Item Removal Focus:** When removing cart items, shift focus to the close button for logical positioning
- **Quantity Changes:** Maintain focus on quantity controls during updates to prevent focus loss
**Screen Reader Interaction:**
- Activator should announce "dialog popup" when focused
- On activation, announce "{Cart label}, dialog" when focus moves to cart drawer
- Provide clear navigation through cart content
- Announce return to activator when drawer closes
- **Quantity Updates:** Use aria-live="polite" on quantity inputs to announce value changes
- **Item Removal:** Announce item removal with descriptive text (e.g., "Product Name removed from cart")
- **Dynamic Content:** Ensure all cart state changes are announced to screen readers
**Structure Requirements:**
- All interactive elements must be descendants of the cart drawer container
- Position close button first in DOM order within the cart drawer container
- Use semantic HTML within the cart drawer (headings, buttons, form labels)
- Provide clear visual focus indicators
- Close buttons should use aria-label="Close cart" with × entity for visual 'x' icon
**Implementation Patterns:**
**Cart Activator Button:**
```html
3
```
**Cart Drawer Container:**
```html
×
Shopping Cart
Total: $99.99
Checkout
```
**Quantity Controls with aria-live:**
```html
-+
```
**Item Removal with Focus Management:**
```javascript
function removeItem(itemId) {
const item = cartItems.find(item => item.id === itemId);
if (item) {
cartItems = cartItems.filter(item => item.id !== itemId);
// Remove only the specific cart item element
const cartItem = document.querySelector(`[data-item-id="${itemId}"]`);
if (cartItem) {
cartItem.remove();
}
// Update cart state
updateCartCount();
updateCartTotal();
// Announce removal to screen readers
announceToScreenReader(`${item.name} removed from cart`);
// Shift focus to close button for logical focus management
const closeButton = document.querySelector('.cart-close');
if (closeButton) {
closeButton.focus();
}
}
}
```
**JavaScript Considerations:**
- Implement proper event listeners for Escape key
- Manage body scroll when cart drawer is open
- Handle focus restoration on cart drawer close
- Implement focus trapping within cart drawer
- Store reference to activator for focus return
- Handle dynamic cart content updates
- Ensure proper announcement of cart state changes
- **Quantity Management:** Update only specific DOM elements instead of re-rendering entire cart
- **Focus Management:** Shift focus to close button when removing items
- **Performance:** Avoid unnecessary DOM manipulation to maintain accessibility features
**Quantity Controls and Item Management:**
- **aria-live="polite":** Add to all quantity input fields for screen reader announcements
- **Focus Preservation:** Maintain focus on quantity controls during updates to prevent keyboard navigation issues
- **Value Announcements:** Screen readers should announce new quantity values when inputs change
- **Item Removal Focus:** Shift focus to close button after removing items for logical navigation flow
- **Dynamic Updates:** Avoid full cart re-rendering to preserve focus and improve performance
**Ecommerce-Specific Considerations:**
- Announce cart item count changes
- Provide clear product information in cart items
- Ensure checkout button is prominently accessible
- Handle empty cart states appropriately
- Provide clear pricing and total information
- Support quantity adjustments with proper labeling
- Handle cart item removal with confirmation
**Accessibility Notes:**
- Cart drawers should not contain critical page navigation
- Ensure cart content is fully accessible to screen readers
- Test with screen readers to ensure proper announcement
- Consider using aria-live regions for dynamic cart updates
- Provide clear error messages for cart operations
- Ensure cart drawer works with keyboard-only navigation
- Test focus management with multiple cart activators
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/chat-window-accessibility.mdc
================================================
---
description: Chat window component accessibility compliance and WCAG compliance for real-time communication features
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Chat Window Component Accessibility Standards
Ensures chat window components follow WCAG compliance and provide proper accessibility for real-time communication, notifications, and assistive technology support.
name: chat_window_accessibility_standards
description: Enforce chat window component accessibility standards and WCAG compliance for real-time communication features
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts|css|scss|sass|less)$"
actions:
- type: enforce
conditions:
# Chat window missing proper landmark role
- pattern: "(?i)<(div|section)[^>]*(?:chat|conversation|messages)[^>]*>"
pattern_negate: "role=\"(log|region|dialog)\""
message: "Chat window containers must have role='log' for message history, role='region' for general chat areas, or role='dialog' for popup windows."
# Chat log missing accessible name
- pattern: "(?i)<[^>]*role=\"log\"[^>]*>"
pattern_negate: "(aria-labelledby|aria-label)"
message: "Chat log elements must have accessible names via aria-labelledby or aria-label for screen reader identification."
# Chat messages missing author identification
- pattern: "(?i)<(p|div)[^>]*(?:message|chat|conversation)[^>]*>"
pattern_negate: "(aria-label|aria-labelledby|class.*visually-hidden|class.*sr-only)"
message: "Chat messages must include author identification via visible text, aria-label, or visually hidden content for screen reader users."
# Chat messages in wrong DOM order
- pattern: "(?i)
]*class=\"[^\"]*(?:left|right|user|agent)[^\"]*\"[^>]*>"
pattern_negate: "(role=\"log\"|aria-live)"
message: "Chat messages must maintain chronological order in DOM structure. Avoid using separate columns that break reading sequence."
# Chat notifications relying only on sound
- pattern: "(?i)(audio|sound|play|new.*message)"
pattern_negate: "(aria-live|role=\"status\"|visual.*notification|title.*change|notification.*badge)"
message: "Chat notifications must use multiple sensory channels. Combine sound with visual indicators, aria-live announcements, title changes, and notification badges."
# Chat window missing keyboard support
- pattern: "(?i)<(div|section)[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown|tabindex)"
message: "Chat windows must support keyboard navigation for opening, closing, and message interaction."
# Chat popup missing focus management
- pattern: "(?i)<(div|section)[^>]*role=\"dialog\"[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(aria-modal|focus|tabindex)"
message: "Chat popup windows must implement proper focus management and aria-modal='true' for accessibility."
# Chat launcher missing aria-haspopup
- pattern: "(?i)<(button|div)[^>]*(?:chat|conversation|support)[^>]*>"
pattern_negate: "aria-haspopup=\"dialog\""
message: "Chat launcher buttons must include aria-haspopup='dialog' to inform users a dialog will open."
# Chat window missing focus trap
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(focus.*trap|tabindex|aria-modal)"
message: "Chat popup windows must implement focus trapping to prevent keyboard navigation outside the dialog."
# Chat input missing proper labeling
- pattern: "(?i)<(input|textarea)[^>]*(?:chat|message|reply)[^>]*>"
pattern_negate: "(aria-label|aria-labelledby|label|placeholder)"
message: "Chat input fields must have proper labels via label elements, aria-label, or aria-labelledby for screen reader context."
# Chat button missing accessible name
- pattern: "(?i)<(button|div)[^>]*(?:chat|conversation|support)[^>]*>"
pattern_negate: "(aria-label|aria-labelledby|>.*[A-Za-z]{10,})"
message: "Chat buttons must have accessible names via visible text, aria-label, or aria-labelledby for screen reader identification."
# Chat button missing proper role
- pattern: "(?i)<(div|span)[^>]*(?:chat|conversation|support)[^>]*>"
pattern_negate: "(role=\"button\"|]*(?:chat|conversation)[^>]*>"
pattern_negate: "aria-expanded=\"(true|false)\""
message: "Expandable chat windows must have aria-expanded attribute to indicate open/closed state for screen readers."
# Chat window missing aria-controls
- pattern: "(?i)<(button|div)[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "aria-controls=\"[^\"]+\""
message: "Chat controls must have aria-controls referencing the chat window ID for proper association."
# Chat timeout missing user notification
- pattern: "(?i)(timeout|session.*expir|inactive)"
pattern_negate: "(notification|warning|extend|20.*second)"
message: "Chat timeouts must notify users before expiration and provide at least 20 seconds to extend the session."
# Chat auto-update missing pause controls
- pattern: "(?i)(auto.*update|real.*time|live.*update)"
pattern_negate: "(pause|stop|hide|user.*control)"
message: "Auto-updating chat content must provide pause/stop controls to prevent overwhelming screen reader users."
# Chat missing persistent notification system
- pattern: "(?i)<(div|section)[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(notification.*badge|aria-live.*polite|role.*status)"
message: "Chat windows must provide persistent notification system including visual badge counter and persistent screen reader announcements when closed."
# Chat focus not contained within popup
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(focus.*trap|tabindex|aria-modal)"
message: "Chat popup windows must contain keyboard focus to prevent focus from becoming obscured behind the dialog."
# Chat messages missing proper contrast
- pattern: "(?i)<(p|div)[^>]*(?:message|chat)[^>]*>"
pattern_negate: "(color.*#[0-4][0-9a-fA-F]{5}|color.*#[5-9a-fA-F][0-9a-fA-F]{5})"
message: "Chat message text must meet WCAG contrast requirements (4.5:1 minimum for normal text, 3:1 for large text)."
# Chat window not responsive
- pattern: "(?i)<(div|section)[^>]*(?:chat|conversation)[^>]*>"
pattern_negate: "(max-width|min-width|flex|grid|responsive)"
message: "Chat windows must be responsive and work on devices down to 320px width without horizontal scrolling."
# Chat icon missing alt text
- pattern: "(?i)]*(?:chat|conversation|support)[^>]*>"
pattern_negate: "alt=\"[^\"]+\""
message: "Chat-related images must have descriptive alt text for screen reader users."
# Chat status messages missing role
- pattern: "(?i)<(div|span)[^>]*(?:status|notification|typing)[^>]*>"
pattern_negate: "role=\"(status|log|alert)\""
message: "Chat status messages must use appropriate ARIA roles (status, log, or alert) for screen reader announcements."
- type: suggest
message: |
**WCAG Chat Window Accessibility Requirements:**
**Key Accessibility Features:**
- **Easy to find and use:** Intuitive placement and clear labeling
- **Multi-sensory notifications:** Visual, auditory, and haptic feedback
- **Universal access:** All features available to all users
- **Message preservation:** All messages accessible to everyone
**WCAG Criteria Implementation:**
**1. Info and Relationships (WCAG 1.3.1):**
- **Author identification:** Each message must clearly indicate who sent it
- **Visual relationships:** Use semantic markup, not just visual styling
- **Screen reader support:** Provide context for message ownership
**2. Meaningful Sequence (WCAG 1.3.2):**
- **Chronological order:** Messages must appear in DOM order matching visual order
- **Avoid columns:** Don't separate user/agent messages into different containers
- **Reading flow:** Maintain logical conversation sequence for screen readers
**3. Sensory Characteristics (WCAG 1.3.3):**
- **Multiple notification methods:** Combine sound, visual, and screen reader announcements
- **No single sense dependency:** Don't rely solely on audio or visual cues
- **Universal alerts:** Ensure notifications work for all users
**4. Orientation (WCAG 1.3.4):**
- **Responsive design:** Work in both portrait and landscape modes
- **No orientation locks:** Allow users to choose their preferred orientation
- **Flexible layouts:** Adapt to different screen sizes and orientations
**5. Contrast (WCAG 1.4.3):**
- **Text contrast:** 4.5:1 minimum for normal text, 3:1 for large text
- **Background contrast:** Ensure sufficient contrast for all text elements
- **Visual clarity:** Make messages easily readable for all users
**6. Reflow (WCAG 1.4.10):**
- **Responsive design:** Work on devices down to 320px width
- **No horizontal scroll:** Content must fit without horizontal scrolling
- **Flexible layouts:** Use CSS Grid and Flexbox for adaptability
**7. Non-text Contrast (WCAG 1.4.11):**
- **Image alternatives:** Provide alt text for all images and icons
- **Icon identification:** Ensure chat icons are properly labeled
- **Visual elements:** Make all non-text content accessible
**8. Keyboard (WCAG 2.1.1):**
- **Full keyboard support:** All chat functions must work with keyboard
- **Navigation:** Support Tab, Enter, Space, and Escape keys
- **No mouse dependency:** Ensure keyboard-only users can fully participate
**9. No Keyboard Trap (WCAG 2.1.2):**
- **Focus management:** Proper focus trapping within chat windows
- **Escape mechanism:** Provide clear way to exit chat
- **Focus restoration:** Return focus to launcher when chat closes
- **Focus trap:** Prevent keyboard navigation outside chat window while open
**11. Timing Adjustable (WCAG 2.2.1):**
- **Session timeouts:** Notify users before session expiration (minimum 30 seconds notice)
- **Extension options:** Provide at least 20 seconds to extend
- **Character monitoring:** Listen for input changes, not just key presses
- **Screen reader notification:** Use dedicated role="alert" element for important timeout warnings
**12. Pause, Stop, Hide (WCAG 2.2.2):**
- **Content control:** Allow users to pause auto-updating content
- **Overwhelm prevention:** Provide mechanisms to reduce screen reader noise
- **Essential content:** Balance accessibility with functionality
**13. Enhanced Notifications (WCAG 4.1.3):**
- **Persistent notifications:** Provide visual badge counter on chat launcher when closed
- **Screen reader announcements:** Persistent aria-live regions for notifications when chat window is closed
- **Multi-sensory feedback:** Combine visual, auditory, and screen reader notifications
- **Context awareness:** Notify users about new messages even when chat window is not open
- **Enhanced button labeling:** Dynamic accessible name that includes unread message count for better context
**14. User Interaction Monitoring (WCAG 2.2.1):**
- **Session extension on interaction:** Extend chat session on any user interaction, not just sent messages
- **Comprehensive monitoring:** Monitor clicks, key presses, input changes, scrolling, and focus events
- **Assistive technology support:** Ensure users with typing difficulties can extend sessions through other interactions
- **Immediate response:** Reset session timer immediately on any user engagement
**15. Focus Not Obscured (WCAG 2.4.11):**
- **Focus visibility:** Ensure keyboard focus is always visible
- **Modal containment:** Keep focus within chat popup windows
- **Background prevention:** Don't let focus move behind chat windows
**16. Name, Role, Value (WCAG 4.1.2):**
- **Accessible names:** Clear labels for all interactive elements
- **Proper roles:** Use semantic HTML or appropriate ARIA roles
- **State management:** Maintain and communicate current states
**17. Status Messages (WCAG 4.1.3):**
- **Real-time updates:** Announce new messages and status changes
- **Context awareness:** Provide appropriate level of detail
- **Screen reader support:** Use role='log' for message history
**18. Modal Dialog Requirements:**
- **aria-haspopup="dialog":** Set on chat launcher buttons (no aria-controls or aria-expanded needed)
- **Focus trapping:** Prevent keyboard navigation outside chat window
- **Initial focus:** Move focus to first focusable element when chat opens
- **Focus restoration:** Return focus to launcher when chat closes
- **Escape key:** Close chat window with Escape key
- **Focus trap implementation:** Must handle both forward (Tab) and backward (Shift+Tab) navigation
- **Focusable element detection:** Include all interactive elements (buttons, inputs, textareas, links, custom focusable elements)
- **Safety checks:** Additional validation to ensure focus remains within chat window
**Implementation Patterns:**
**1. Basic Chat Window Structure (Modal Dialog Pattern):**
```html
Chat with Support
0
**Note:** For modal dialog pattern, use only `aria-haspup="dialog"`. Do not use `aria-controls` or `aria-expanded` as these are not needed for modal dialogs.
Chat: Customer Support
×
Customer Support:
Hi, how can I help you today?
You sent:
I'm having trouble resetting my password.
Send
```
**2. Chat Message Structure:**
```html
Customer Support:
Hi, how can I help you today?
You sent:
I'm having trouble resetting my password.
Customer Support:
I can help with that. What's your email address?
**Note:** Use div elements for chat messages instead of role="article" to reduce navigation noise for screen reader users. Chat messages are conversational content rather than standalone articles.
Customer Support: Hi
Customer Support: How can I help?
Me: Hi
Me: I need help
```
**3. Multi-Sensory Notifications:**
```html
New message received
1
```
**4. Enhanced Notification System:**
```html
Chat with Support
0
**Note:** The notification badge is part of the button's accessible name, not hidden from screen readers. This provides better context about unread messages.
**5. Enhanced Button Labeling:**
```html
Chat with Support
0
```
**JavaScript for Dynamic Labeling:**
```javascript
// Update button's accessible name to include message count
function updateNotificationBadge() {
const launcher = document.getElementById('chat-launcher');
if (messageCount === 1) {
launcher.setAttribute('aria-label', 'Chat with Support - 1 new message');
} else if (messageCount > 1) {
launcher.setAttribute('aria-label', `Chat with Support - ${messageCount} new messages`);
}
}
// Reset button's accessible name when chat is opened
function clearNotificationBadge() {
const launcher = document.getElementById('chat-launcher');
launcher.removeAttribute('aria-label');
}
```
```
**6. Focus Management:**
```html
Chat Support
×
```
**7. Session Timeout Management:**
```html
This chat will end in 20 seconds if you do not reply.
Extend Session
This chat will end in 20 seconds if you do not reply.
```
**Note:** The timeout warning should appear with sufficient notice (minimum 30 seconds) and use a dedicated `role="alert"` element for screen reader announcements. This ensures important timeout information is properly communicated without interfering with chat functionality.
**8. User Interaction Monitoring:**
```javascript
// Setup comprehensive user interaction monitoring
function setupUserInteractionListeners() {
const chatWindow = document.getElementById('chat-window');
// Add listeners for various user interactions
chatWindow.addEventListener('click', extendSessionOnInteraction);
chatWindow.addEventListener('keydown', extendSessionOnInteraction);
chatWindow.addEventListener('keypress', extendSessionOnInteraction);
chatWindow.addEventListener('input', extendSessionOnInteraction);
chatWindow.addEventListener('scroll', extendSessionOnInteraction);
// Also listen for focus events on interactive elements
const interactiveElements = chatWindow.querySelectorAll('button, input, textarea, [tabindex]:not([tabindex="-1"])');
interactiveElements.forEach(element => {
element.addEventListener('focus', extendSessionOnInteraction);
});
}
```
**Note:** Chat sessions should extend on any user interaction, not just sent messages. This is crucial for assistive technology users who may have difficulty typing or need more time to compose messages. Monitor clicks, key presses, input changes, scrolling, and focus events to ensure the session remains active during user engagement.
**Important:** When extending sessions, ensure both the main session timeout and warning timeout are properly cleared to prevent race conditions where the session can end even after being extended.
**Session State Management:** Use a dedicated boolean flag (`isSessionExtended`) to track session extension state, rather than relying on DOM element visibility. This prevents race conditions and ensures reliable session management.
**Race Condition Prevention:** Set the session extension flag BEFORE clearing timeouts to prevent race conditions where the countdown might still call `endSession()` after the session has been extended.
**Countdown Safety:** Implement proper bounds checking to prevent negative countdown values and ensure the countdown stops immediately when the session is extended. Clear existing intervals before starting new ones to prevent multiple countdowns from running simultaneously.
**Session Ending Protection:** Implement multiple safety checks in the `endSession()` function to prevent premature session termination. Check session extension status, session activity state, and warning visibility before allowing the session to end.
**Visual Design Consistency:** Follow the established design pattern used across all accessibility example files, including consistent container styling, typography, color scheme, and layout structure for a cohesive user experience.
**9. Responsive Chat Design:**
```css
/* Responsive chat window */
.chat-window {
position: fixed;
bottom: 20px;
right: 20px;
width: 350px;
height: 500px;
max-width: calc(100vw - 40px);
max-height: calc(100vh - 40px);
background: white;
border: 1px solid #ccc;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: flex;
flex-direction: column;
}
/* Mobile responsive adjustments */
@media screen and (max-width: 480px) {
.chat-window {
bottom: 0;
right: 0;
width: 100%;
height: 100vh;
max-width: none;
max-height: none;
border-radius: 0;
}
}
/* Ensure minimum width compliance */
@media screen and (max-width: 320px) {
.chat-window {
width: 320px;
min-width: 320px;
}
}
```
**10. Screen Reader Optimized Messages:**
```html
Customer Support:
Hi, how can I help you today?
Customer Support is typing...
```
**11. Proper Focus Trap Implementation:**
```javascript
// Focus trap variables
let focusableElements = [];
let firstFocusableElement = null;
let lastFocusableElement = null;
// Setup focus trap
function setupFocusTrap() {
const chatWindow = document.getElementById('chat-window');
// Get all focusable elements within the chat window
// Include all interactive elements that can receive focus
focusableElements = chatWindow.querySelectorAll(
'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [role="button"]:not([disabled])'
);
if (focusableElements.length > 0) {
firstFocusableElement = focusableElements[0];
lastFocusableElement = focusableElements[focusableElements.length - 1];
// Add focus trap event listener
chatWindow.addEventListener('keydown', trapFocus);
}
}
// Focus trap functionality
function trapFocus(event) {
if (event.key === 'Tab') {
// Check if focus is currently within the chat window
const chatWindow = document.getElementById('chat-window');
const isFocusInChat = chatWindow.contains(document.activeElement);
if (event.shiftKey) {
// Shift + Tab: move backwards
if (document.activeElement === firstFocusableElement) {
event.preventDefault();
lastFocusableElement.focus();
}
} else {
// Tab: move forwards
if (document.activeElement === lastFocusableElement) {
event.preventDefault();
firstFocusableElement.focus();
}
}
// Additional safety check: if focus somehow escapes, bring it back
if (!isFocusInChat && focusableElements.length > 0) {
event.preventDefault();
firstFocusableElement.focus();
}
}
}
// Clean up focus trap when closing chat
function closeChat() {
const chatWindow = document.getElementById('chat-window');
const launcher = document.getElementById('chat-launcher');
chatWindow.hidden = true;
launcher.setAttribute('aria-expanded', 'false');
// Remove focus trap
chatWindow.removeEventListener('keydown', trapFocus);
// Return focus to launcher
launcher.focus();
}
```
**JavaScript Considerations:**
- Implement proper focus management for popup windows
- Handle keyboard navigation (Tab, Enter, Escape)
- Manage session timeouts with user notifications
- Provide pause/stop controls for auto-updating content
- Update page titles for new message notifications
- Monitor input activity for session extension
- Handle focus restoration when chat closes
- Implement proper ARIA live regions for status updates
**Accessibility Notes:**
- Use role="log" for message history with aria-labelledby
- Provide multi-sensory notifications for new messages
- Maintain chronological message order in DOM
- Ensure all interactive elements have accessible names
- Test with screen readers for proper announcement
- Consider user preferences for notification levels
- Provide alternatives to chat (phone, email, etc.)
- Test keyboard navigation thoroughly
- Validate focus management in popup windows
- Use div elements for chat messages to reduce navigation noise (avoid role="article")
- Chat messages are conversational content, not standalone articles
**Testing Requirements:**
- Test with screen readers (NVDA, JAWS, VoiceOver)
- Verify keyboard navigation works completely
- Test focus management in popup windows
- Validate message reading order
- Test timeout notifications and extensions
- Verify responsive design on small screens
- Test with different notification preferences
- Validate ARIA live region announcements
- Test focus restoration when chat closes
- Verify contrast ratios meet WCAG requirements
- **Test focus trapping thoroughly:** Verify Tab and Shift+Tab navigation stays within chat window
- **Test focus escape scenarios:** Ensure focus cannot accidentally leave the chat window
- **Test focus restoration:** Verify focus returns to launcher when chat closes
- **Test all interactive elements:** Ensure all buttons, inputs, and focusable elements are included in focus trap
**Common Mistakes to Avoid:**
- Chat messages in wrong DOM order
- Notifications relying only on sound
- Missing keyboard support for chat functions
- Poor focus management in popup windows
- Missing author identification in messages
- Chat windows not responsive to small screens
- Missing timeout warnings and extensions
- Auto-updating content without pause controls
- Focus becoming obscured behind chat windows
- Missing accessible names for chat controls
- Chat buttons without proper roles
- Missing expandable state management
- Insufficient contrast for message text
- Chat icons without alt text
- Missing status message roles
- Missing aria-haspopup="dialog" on chat launcher
- Missing focus trapping in chat popup windows
- Poor focus management when opening/closing chat
- **Incomplete focus trap implementation:** Focus trap that only works in one direction (Tab vs Shift+Tab)
- **Missing focusable elements:** Focus trap that doesn't include all interactive elements
- **Focus escape vulnerabilities:** Focus trap without safety checks to prevent focus from leaving chat window
- **Incorrect ARIA attributes for modal dialogs:** Using aria-controls or aria-expanded with modal dialog pattern
- **Missing notification context:** Not including unread message count in button's accessible name
- **Decorative images with alt text:** Including unnecessary alt text for decorative icons in buttons
- **Limited session extension:** Only extending chat session on sent messages instead of all user interactions
- **Missing user interaction monitoring:** Not monitoring clicks, key presses, scrolling, and focus events for session extension
metadata:
priority: high
version: 1.0
description:
globs:
alwaysApply: false
---
================================================
FILE: .cursor/rules/color-contrast-accessibility.mdc
================================================
---
description: Text and user interface color contrast compliance with WCAG 2.2 1.4.3 and 1.4.11
globs: *.css, *.scss, *.sass, *.less, *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts
alwaysApply: true
---
# Color Contrast Accessibility Standards
Ensures color contrast meets WCAG 2.2 1.4.3: Contrast (Minimum) and 1.4.11: Non-text Contrast requirements.
name: color_contrast_accessibility_standards
description: Enforce color contrast accessibility standards per WCAG 2.2 1.4.3 and 1.4.11
filters:
- type: file_extension
pattern: "\\.(css|scss|sass|less|vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Common low-contrast text color combinations
- pattern: "color:\\s*#[89abcdefABCDEF]{6}"
message: "Light text colors may not meet 4.5:1 contrast ratio requirement. Verify contrast against background."
- pattern: "color:\\s*#[0-6]{6}"
message: "Very light text colors likely fail contrast requirements. Use darker colors for better accessibility."
# Light gray text (common accessibility issue)
- pattern: "color:\\s*(#[cdefCDEF]{3,6}|lightgray|lightgrey|silver)"
message: "Light gray text often fails WCAG contrast requirements (4.5:1 minimum). Use darker colors."
# Common problematic color combinations
- pattern: "background.*#fff.*color.*#[89abcdefABCDEF]"
message: "Light text on white background may not meet 4.5:1 contrast ratio requirement."
- pattern: "background.*#f[0-9a-fA-F]{5}.*color.*#[89abcdefABCDEF]"
message: "Light text on light background may not meet contrast requirements."
# UI component border/focus indicators
- pattern: "border.*#[cdefCDEF]{3,6}"
message: "Light borders may not meet 3:1 non-text contrast requirement for UI components."
- pattern: "outline.*#[cdefCDEF]{3,6}"
message: "Light focus outlines may not meet 3:1 contrast requirement for UI component identification."
# Button states with insufficient contrast
- pattern: "button.*background.*#[cdefCDEF]{3,6}"
message: "Light button backgrounds may not provide sufficient 3:1 contrast for UI component identification."
# Form input borders
- pattern: "input.*border.*#[defDEF]{3,6}"
message: "Very light input borders may not meet 3:1 contrast requirement for form field identification."
# SVG icon fill colors that may lack contrast
- pattern: "fill=\"#[cdefCDEF]{3,6}\""
message: "Light SVG fill colors may not meet 3:1 contrast requirement for icon identification."
# SVG stroke colors for icon outlines
- pattern: "stroke=\"#[defDEF]{3,6}\""
message: "Very light SVG stroke colors may not provide sufficient contrast for icon visibility."
# Missing prefers-contrast considerations
- pattern: "@media\\s*\\(prefers-contrast:\\s*more\\)"
pattern_negate: "color|background|border"
message: "prefers-contrast: more media query should include enhanced color/contrast properties."
- type: suggest
message: |
**WCAG 2.2 Color Contrast Requirements:**
**1.4.3: Text Contrast (Minimum) - Level AA:**
- **Normal Text:** Minimum 4.5:1 contrast ratio
- **Large Text:** Minimum 3:1 contrast ratio (18pt+ regular or 14pt+ bold)
- **Enhanced (Level AAA):** 7:1 for normal text, 4.5:1 for large text
**1.4.11: Non-text Contrast - Level AA:**
- **UI Components:** Minimum 3:1 contrast ratio for component identification
- **Focus Indicators:** Minimum 3:1 contrast ratio for focus visibility
- **Graphical Objects:** Minimum 3:1 contrast ratio for content understanding
**Exceptions (No Contrast Requirement):**
- Inactive/disabled UI components
- Pure decorative elements
- Text in logos or brand names
- Text that is not visible to users
- Graphics where specific presentation is essential
**High Contrast Color Combinations:**
**Dark Text on Light Backgrounds:**
- `#212529` on `#ffffff` - 16.6:1 ✅
- `#495057` on `#ffffff` - 8.3:1 ✅
- `#6c757d` on `#ffffff` - 5.4:1 ✅
- `#343a40` on `#f8f9fa` - 11.7:1 ✅
**Light Text on Dark Backgrounds:**
- `#ffffff` on `#212529` - 16.6:1 ✅
- `#f8f9fa` on `#495057` - 7.0:1 ✅
- `#ffffff` on `#0056b3` - 7.7:1 ✅
- `#ffffff` on `#dc3545` - 5.8:1 ✅
**UI Component Colors (3:1 minimum):**
- Focus outlines: `#0056b3`, `#dc3545`, `#198754`
- Border colors: `#ced4da`, `#adb5bd`, `#6c757d`
- Button states: `#0056b3`, `#157347`, `#b02a37`
**Implementation Examples:**
**CSS Text Contrast:**
```css
/* Good: High contrast text */
.primary-text {
color: #212529;
background: #ffffff;
}
.secondary-text {
color: #495057;
background: #ffffff;
}
/* Good: Large text with 3:1 minimum */
.large-heading {
font-size: 18px;
font-weight: normal;
color: #6c757d;
background: #ffffff;
}
```
**CSS UI Component Contrast:**
```css
/* Good: Form inputs with sufficient border contrast */
.form-control {
border: 2px solid #ced4da; /* 3:1+ contrast */
background: #ffffff;
}
.form-control:focus {
border-color: #0056b3; /* High contrast focus */
outline: 3px solid #0056b3;
outline-offset: 2px;
}
/* Good: Button states */
.btn-primary {
background: #0056b3;
color: #ffffff;
border: 1px solid #004085;
}
```
**Contrast Testing:**
- Use browser dev tools color picker for contrast ratios
- Test with tools like WebAIM Contrast Checker
- Verify with automated accessibility testing tools
- Test with actual users who have visual impairments
**Common Problematic Combinations to Avoid:**
- Light gray text (#999999) on white backgrounds
- Yellow text (#ffff00) on white backgrounds
- Light blue links (#87ceeb) on white backgrounds
- Thin borders (#e0e0e0) for essential UI components
- Low contrast placeholder text
- Insufficient focus indicator contrast
**Responsive Considerations:**
- Maintain contrast ratios across all screen sizes
- Consider dark mode color schemes
- Test contrast in different lighting conditions
- Ensure contrast is maintained with CSS filters/effects
**CSS `prefers-contrast` Media Query:**
The `prefers-contrast` media query allows adaptation to user contrast preferences:
```css
/* Default styles */
.button {
background: #0056b3;
color: #ffffff;
border: 1px solid #004085;
}
/* High contrast preference */
@media (prefers-contrast: more) {
.button {
background: #000000;
color: #ffffff;
border: 2px solid #ffffff;
font-weight: bold;
}
.text-secondary {
color: #000000; /* Increase from gray to black */
}
.form-control {
border: 3px solid #000000; /* Thicker, darker borders */
}
}
/* Low contrast preference (rare) */
@media (prefers-contrast: less) {
.high-contrast-element {
/* Reduce contrast only where specifically needed */
/* Still maintain minimum WCAG requirements */
}
}
/* Custom contrast preference */
@media (prefers-contrast: custom) {
/* Allow user-defined contrast settings */
/* Respect system/browser contrast customizations */
}
```
**Icon Accessibility & Contrast Requirements:**
Icons are UI components and must meet 3:1 contrast ratio for identification:
**SVG Icon Examples:**
```html
/* CSS for icon button with 3:1 contrast */
.icon-button {
background: #0056b3; /* High contrast background */
border: 2px solid #004085; /* Visible border for component identification */
padding: 8px;
border-radius: 4px;
}
.icon-button:focus {
outline: 3px solid #ffd700; /* High contrast focus indicator */
outline-offset: 2px;
}
```
**Icon Contrast Considerations:**
- **Informative Icons:** Must meet 3:1 contrast against background
- **Decorative Icons:** No contrast requirement (use `aria-hidden="true"`)
- **Icon Buttons:** Both icon and button background need sufficient contrast
- **State Icons:** Different states (active/inactive) need distinguishable contrast
- **Icon + Text:** Ensure both elements meet their respective requirements
**Complex Icon Examples:**
```css
/* Multi-state icon button */
.toggle-icon {
background: #f8f9fa;
border: 2px solid #6c757d; /* 3:1+ contrast for UI component */
color: #495057;
}
.toggle-icon[aria-pressed="true"] {
background: #0056b3;
color: #ffffff;
border-color: #004085;
}
/* Icon with adaptive contrast */
@media (prefers-contrast: more) {
.icon-subtle {
filter: contrast(150%); /* Increase icon contrast */
}
.icon-button {
border-width: 3px; /* Thicker borders for better identification */
}
}
```
**Design System Approach:**
- Establish a palette of WCAG-compliant color combinations
- Document contrast ratios for all color pairs
- Create design tokens with built-in accessibility
- Implement automated contrast checking in build process
- Define icon contrast standards for different contexts
- Test icons across different contrast preferences
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/color-swatch-accessibility.mdc
================================================
---
description: Color swatch component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Color Swatch Variant Selector Component Accessibility Standards
Ensures color swatch variant selectors follow WCAG compliance and provide proper accessibility for all users.
name: color_swatch_accessibility_standards
description: Enforce color swatch variant selector accessibility standards and WCAG compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts|css|scss|sass|less)$"
actions:
- type: enforce
conditions:
# Radio buttons hidden with display: none (accessibility blocker)
- pattern: "(?i)input\\[type=\"radio\"\\].*\\{[^}]*display:\\s*none"
message: "Radio buttons must not use display: none as it removes keyboard and screen reader access. Use appearance: none or visually-hidden class instead."
# Color swatches missing accessible labels
- pattern: "(?i)]*type=\"radio\"[^>]*>"
pattern_negate: "(id.*for|label.*for)"
message: "Color swatch radio buttons must have associated label elements via id/for attributes."
# Color-only information without text alternatives
- pattern: "(?i)<[^>]*(?:color|swatch|variant)[^>]*>"
pattern_negate: "(data-label|aria-label|title|data-tooltip|class.*tooltip)"
message: "Color swatches must include text alternatives (data-label, tooltips, labels) as color alone cannot convey information to all users."
# Missing fieldset and legend for variant groups
- pattern: "(?i)]*type=\"radio\"[^>]*name=\"[^\"]*\"[^>]*>"
pattern_negate: "(fieldset|role=\"group\"|aria-labelledby)"
message: "Radio button groups should be wrapped in fieldset with legend or have role='group' with aria-labelledby."
# Missing keyboard navigation support
- pattern: "(?i)<[^>]*(?:color|swatch|variant)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown|tabindex)"
message: "Color swatch components should handle keyboard navigation (arrow keys) for variant selection."
# Missing focus indicators
- pattern: "(?i)<[^>]*(?:color|swatch|variant)[^>]*>"
pattern_negate: "(focus|:focus|outline|box-shadow)"
message: "Color swatch components must have visible focus indicators for keyboard navigation."
# Tooltip missing proper accessibility
- pattern: "(?i)<[^>]*(?:tooltip|title)[^>]*>"
pattern_negate: "(role=\"tooltip\"|aria-describedby|aria-label)"
message: "Color swatch tooltips must have proper ARIA attributes for screen reader accessibility."
- type: suggest
message: |
**Color Swatch Variant Selector Accessibility Best Practices:**
**Required Accessibility Features:**
- **Keyboard Navigation:** Arrow keys (←/→) to navigate between variants
- **Screen Reader Support:** Proper labeling and announcements
- **Focus Indicators:** Highly visible focus states
- **Text Alternatives:** Tooltips or labels for color information
- **Proper Styling:** Use appearance: none instead of display: none
- **Label Focus:** Labels automatically receive focus when their associated input is focused (no tabindex needed)
- **Input Focus:** Radio inputs are naturally focusable and part of tab order (no tabindex needed)
**Implementation Patterns:**
**Basic Color Swatch Structure:**
```html
```
**CSS for Accessible Styling:**
```css
/* Visually hide radio buttons while keeping them accessible */
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Focus Management: Labels automatically receive focus when their input is focused */
/* No tabindex needed on labels - they are naturally focusable through their input association */
/* No tabindex needed on inputs - radio inputs are naturally focusable and part of tab order */
/* Alternative: Use appearance: none for custom styling */
input[type="radio"] {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
/* Custom styling here */
}
/* Color swatch styling */
.color-swatch {
display: inline-block;
width: 40px;
height: 40px;
border: 2px solid transparent;
border-radius: 50%;
cursor: pointer;
position: relative;
transition: border-color 0.2s ease;
}
.color-swatch:focus {
outline: 3px solid #0056b3;
outline-offset: 2px;
border-color: #0056b3;
}
/* Show focus styles on label when input is focused */
.color-swatch:has(+ input:focus) {
outline: 3px solid #0056b3;
outline-offset: 2px;
border-color: #0056b3;
}
/* Alternative: Use :focus-within for broader browser support */
.color-swatch:focus-within {
outline: 3px solid #0056b3;
outline-offset: 2px;
border-color: #0056b3;
}
.color-swatch:has(+ input:checked) {
border-color: #212529;
box-shadow: 0 0 0 2px #fff, 0 0 0 4px #212529;
}
.swatch-color {
width: 100%;
height: 100%;
border-radius: 50%;
display: block;
}
/* Tooltip styling */
.color-swatch::after {
content: attr(data-label);
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
background: #212529;
color: white;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
z-index: 1000;
}
.color-swatch:hover::after,
.color-swatch:focus::after,
.color-swatch:has(+ input:focus)::after {
opacity: 1;
}
```
================================================
FILE: .cursor/rules/combobox-accessibility.mdc
================================================
---
description: Combobox component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Combobox Component Accessibility Standards
Ensures combobox components follow WCAG compliance and WAI-ARIA Combobox Pattern specifications.
name: combobox_accessibility_standards
description: Enforce combobox component accessibility standards and WAI-ARIA Combobox Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Combobox role requirement
- pattern: "(?i)<(div|section)[^>]*(?:combobox|autocomplete)[^>]*>"
pattern_negate: "role=\"combobox\""
message: "Combobox containers must have role='combobox' attribute."
# aria-expanded requirement
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "aria-expanded=\"(true|false)\""
message: "Combobox elements must have aria-expanded attribute set to 'true' or 'false'."
# aria-haspopup requirement
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "aria-haspopup=\"listbox\""
message: "Combobox elements must have aria-haspopup='listbox' attribute."
# aria-controls requirement
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "aria-controls=\"[^\"]+\""
message: "Combobox elements must have aria-controls attribute referencing the ID of the associated listbox."
# aria-autocomplete requirement
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "aria-autocomplete=\"(list|both|inline|none)\""
message: "Combobox elements must have aria-autocomplete attribute set to 'list', 'both', 'inline', or 'none'."
# aria-activedescendant requirement when expanded
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*aria-expanded=\"true\"[^>]*>"
pattern_negate: "aria-activedescendant=\"[^\"]+\""
message: "Expanded combobox elements must have aria-activedescendant attribute referencing the ID of the active option."
# Listbox role requirement
- pattern: "(?i)<(div|ul)[^>]*(?:listbox|dropdown|popup)[^>]*>"
pattern_negate: "role=\"listbox\""
message: "Listbox containers must have role='listbox' attribute."
# Option role requirement
- pattern: "(?i)<(div|li)[^>]*(?:option|item)[^>]*>"
pattern_negate: "role=\"option\""
message: "Listbox options must have role='option' attribute."
# Option ID requirement for aria-activedescendant
- pattern: "(?i)<[^>]*role=\"option\"[^>]*>"
pattern_negate: "id=\"[^\"]+\""
message: "Listbox options must have unique id attributes for aria-activedescendant to reference them."
# aria-selected requirement for options
- pattern: "(?i)<[^>]*role=\"option\"[^>]*>"
pattern_negate: "aria-selected=\"(true|false)\""
message: "Listbox options must have aria-selected attribute set to 'true' or 'false'."
# Missing keyboard event handlers
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Combobox elements should handle keyboard events (Arrow keys, Enter, Escape, etc.)."
# Missing status region
- pattern: "(?i)<[^>]*role=\"combobox\"[^>]*>"
pattern_negate: "aria-controls=\"[^\"]+\".*?<[^>]*role=\"status\""
message: "Combobox should have a status region to announce available options."
- type: suggest
message: |
**Combobox Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **role='combobox':** Set on the input container element
- **aria-expanded:** 'true' if listbox is visible, 'false' if hidden
- **aria-haspopup='listbox':** Indicates the combobox has a listbox popup
- **aria-controls:** Reference to the ID of the associated listbox
- **aria-autocomplete:** 'list', 'both', 'inline', or 'none' based on behavior
- **aria-activedescendant:** Reference to the ID of the currently active option (remove when listbox is hidden)
- **role='listbox':** Set on the popup container element (preferably on a `ul` element)
- **role='option':** Set on each selectable item in the listbox (preferably on an `li` element)
- **id:** Unique ID on each option element for `aria-activedescendant` to reference
- **aria-selected:** 'true' or 'false' on each option
- **role='status':** Set on a visually hidden element to announce available options
**Keyboard Interaction Requirements:**
- **Down Arrow:** Open listbox and move focus to first option
- **Up Arrow:** Open listbox and move focus to last option
- **Enter/Space:** Select focused option and close listbox
- **Escape:** Close listbox without selection
- **Tab:** Move focus to next focusable element
- **Shift+Tab:** Move focus to previous focusable element
- **Home/End:** Move focus to first/last option
- **Character Keys:** Filter options based on input
**Focus Management:**
- Focus should remain on the input while navigating options
- Use aria-activedescendant to indicate the currently focused option
- Return focus to input after selection or closing
- Ensure focus is trapped within the combobox while open
**Status Region Requirements:**
- Must announce number of available options when listbox opens
- Must announce when no options are available
- Must use proper pluralization ("1 item available" vs "2 items available")
- Must be visually hidden but available to screen readers
- Should update dynamically as options are filtered
**Semantic HTML Structure:**
- Use `ul` element for the listbox container
- Use `li` elements for individual options
- This provides better semantic structure and is more appropriate for lists
**Implementation Example:**
```html
Option 1
Option 2
```
**JavaScript Considerations:**
- Implement proper event listeners for all keyboard interactions
- Update ARIA attributes dynamically based on state
- **Remove `aria-activedescendant` when hiding the listbox** to avoid referencing hidden elements
- Handle focus management and trapping
- Implement proper filtering and selection logic
- Update status messages for all state changes
- Ensure proper pluralization in status messages
- Handle edge cases (no matches, empty input, etc.)
**Accessibility Notes:**
- Status region helps screen readers understand available options
- Proper pluralization improves user experience
- Clear status messages help users understand the current state
- Visual feedback should match announced status
- Test with screen readers to ensure proper announcement
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/css-standards.mdc
================================================
---
description: Writing CSS, whether inside .css files or in the `{% stylesheet %}…{% endstylesheet %}` or `{% style %}…{% endstyle %}` tags
globs:
alwaysApply: false
---
# CSS Standards
## Specificity Rules
- **Never** use IDs as selectors
- **Avoid** using elements as selectors
- **Avoid** using `!important` at all costs - if you must use it, comment why in the code
- Use a `0 1 0` specificity wherever possible, meaning a single `.class` selector.
- In cases where you must use higher specificity due to a parent/child relationship, try to keep the specificity to a maximum of `0 4 0`
- Note that this can sometimes be impossible due to the `0 1 0` specificity of pseudo-classes like `:hover`. There may be situations where `.parent:hover .child` is the only way to achieve the desired effect.
- **Avoid** complex selectors. A selector should be easy to understand at a glance. Don't over do it with pseudo selectors (:has, :where, :nth-child, etc).
See [MDN](mdc:https:/developer.mozilla.org/en-US/docs/Web/CSS/Specificity) for more a comprehensive list of specificity rules.
### Notes on `:has()` selector and Shopify themes
The `:has()` selector is incredibly useful, but can impact performance. This is mainly a problem during dynamic DOM updates as the browser engines must re-evaluate `:has()` selectors. This is especially important in Shopify themes where dynamic content updates are common (cart updates, variant selection, filtering, etc.). See [MDN :has performance considerations](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:has#performance_considerations) for more information.
Performance mitigation strategies:
#### Minimize Subtree Traversals
Anchor of an element as close to the children as possible. i.e. `A:has(B)`, where `A` is the anchor.
Use combinators like `>` or `+` so there is a very clear path for the browser to evaluate. Anything too broad increases the number of leaf nodes to verify.
```css
/* ❌ AVOID: May trigger full subtree traversal */
.ancestor:has(.foo) {
/* Any change within .ancestor requires checking ALL descendants */
}
/* ✅ GOOD: More constrained - limits traversal */
.ancestor:has(> .foo) {
/* Only checks direct children */
}
```
#### Leverage server-rendered classes when possible
If the dynamic content is being server rendered, you might be able to write a class higher in the DOM than rely on `:has()`
Example: With filters, instead of checking based on the state of the inner `input` element, create a disabled class.
```css
/* ❌ AVOID: Styling .filter-label based on child */
.filter-label:has(input[disabled]) {
/* Disabled styles */
}
/* ✅ GOOD: .disabled set server side */
.filter-label.disabled {
/* Disabled styles */
}
```
This strategy won't work for client-side events, like a `checked`, `selected`, `focus` event.
## CSS Variables
CSS variables, a.k.a. custom properties, are a powerful tool for reducing redundancy and making it easier to update values across a component.
- If you need to hardcode a value, set it to a variable and use that variable in the declaration. Example: a touch target size. `--touch-target-size: 44px;`
- **Never** hardcode colors, always use the color schemes
### Global Variables
Global variables should be scoped to the `:root` selector in `snippets/theme-styles-variables.liquid`.
**Example of global variables**
```css
/* in snippets/theme-styles-variables.liquid */
:root {
--page-width: 1400px;
--font-body--family: {{ settings.type_body_font.family }}, {{ settings.type_body_font.fallback_families }}; /* Referencing a theme setting */
--font-{{ preset_name_dash }}--family: {{ settings[preset_font] | prepend: 'var(--font-' | append: '--family)' }}; /* Using Liquid to set a variable */
}
```
### Scoped Variables
Be sure to scope your CSS variables to the component they are being used in, if they are not meant to be global. Scoped variables can reference global variables.
**Example of scoped variables**
```css
/* in assets/facets.css */
.facets {
--drawer-padding: var(--padding-md); /* Referencing a global variable */
--facets-upper-z-index: 3;
--facets-open-z-index: 4;
--facets-clear-shadow: 0px -4px 14px 0px rgb(var(--color-foreground-rgb) / var(--opacity-10)); /* Referencing a Color Scheme variable */
}
```
### Namespace Your CSS Variables
Namespace your variables to avoid collisions unless you explicitly want them to bleed through to other components.
✅ Do this:
```css
.component {
--component-padding: ...;
--component-aspect-ratio: ...;
}
```
❌ Don't do this:
```css
.component {
--padding: ...;
--aspect-ratio: ...;
}
```
### Semantic Color Variables
Use semantic naming for better maintainability:
```css
:root {
/* Base colors */
--color-primary: {{ settings.colors_accent_1 }};
--color-secondary: {{ settings.colors_accent_2 }};
/* Semantic colors */
--color-text-primary: rgb(var(--color-foreground));
--color-text-secondary: rgb(var(--color-foreground) / 0.75);
--color-text-disabled: rgb(var(--color-foreground) / 0.38);
/* Interactive states */
--color-interactive-default: rgb(var(--color-accent));
/* color-mix isn't supported in earlier version of iOS <16.2 so limit its usage to progressive enhancement */
--color-interactive-hover: color-mix(in srgb, rgb(var(--color-accent)) 90%, black);
--color-interactive-pressed: color-mix(in srgb, rgb(var(--color-accent)) 80%, black);
--color-interactive-disabled: rgb(var(--color-accent) / 0.38);
}
```
### Design Token System
Establish consistent spacing and typography scales:
```css
:root {
/* Spacing scale */
--space-3xs: 0.25rem; /* 4px */
--space-2xs: 0.5rem; /* 8px */
--space-xs: 0.75rem; /* 12px */
--space-sm: 1rem; /* 16px */
--space-md: 1.5rem; /* 24px */
--space-lg: 2rem; /* 32px */
--space-xl: 3rem; /* 48px */
--space-2xl: 4rem; /* 64px */
--space-3xl: 6rem; /* 96px */
/* Typography scale */
--font-size-xs: 0.75rem; /* 12px */
--font-size-sm: 0.875rem; /* 14px */
--font-size-base: 1rem; /* 16px */
--font-size-lg: 1.125rem; /* 18px */
--font-size-xl: 1.25rem; /* 20px */
--font-size-2xl: 1.5rem; /* 24px */
--font-size-3xl: 1.875rem; /* 30px */
}
```
## Scoping CSS to Instances of Sections and Blocks
Reset CSS variable values inline on a `style` attribute with a section/block settings. This has a couple benefits:
- Less CSS in Liquid which allows us to use the `{% stylesheet %}` tag for all CSS.
- Reduces redundancy in CSS selectors and number of selectors in the HTML, i.e. `.selector--{{ block.id }}` pattern.
✅ Do this:
```html
...
...
```
❌ Don't do this:
```html
{% style %} .selector--{{ block.id }} { --button-color: {{ settings.button_color }}; } {% endstyle %}
...
```
### Redundancy
Use variables to reduce property assignment redundancy.
```css
/* Do this */
.block-name {
background: rgb(var(--block-name-color) / 0.75);
}
.block-name--secondary {
--block-name-color: var(--secondary-color);
}
/* Not this */
.block-name {
background: rgb(var(--primary-color) / 0.75);
}
.block-name--secondary {
background: rgb(var(--secondary-color) / 0.75);
}
```
## BEM Naming Convention
Use the @BEM CSS convention for class names.
BEM TL;DR:
- **Block**: Component name (`.product-card`)
- **Element**: Block + element (`.product-card__title`)
- **Modifier**: Block/element + modifier (`.product-card--featured`)
- **Use dashes** to separate words in names
```css
/* Good BEM structure */
.product-card {
}
.product-card__image {
}
.product-card__title {
}
.product-card__price {
}
.product-card--featured {
}
.product-card__title--large {
}
```
```css
.block {
...;
}
.block--modifier {
...;
}
.block__element {
...;
}
.block__multi-word-element {
...;
}
.block__element--modifier {
...;
}
.block__element--multi-word-modifier {
...;
}
```
Dashes are used to separate words in blocks, elements, and modifiers.
Exception: We also use global @utility classes that can be applied to block and and elements without following BEM naming convention.
### Naming a "Block" (component)
The root "block" namespace must wrap any elements derived from it.
✅ Do this:
```html
```
❌ Not this:
`.my-component__wrapper` is used as a parent to `.my-component`.
```html
```
### Naming an "Element" (child)
There should only be a _single_ "element" in a classname. Only the root "block" name needs to be included in child classnames. If additional naming specificity is necessary, use a "-" to seperate words or consider starting a new BEM scope altogether when an element could make sense as a standalone entity.
✅ Do this:
```html
My button
```
✅ Or this:
Started new scope with `.button-component`.
```html
My button
```
❌ Not this:
Multiple element names are used (`__wrapper__button__label`).
```html
My button
```
### Naming a "Modifier" (variant)
Any "modifier" classname should always use a "--" and should always correspond to an existing block and element namespace. Never use a modifier class on an element that doesn't also have a base classname.
✅ Do this:
The `.button` class is the base classname and modified by `--secondary`.
```html
```
❌ Not this:
The `.button` and `.button-secondary` classes are both named as _exclusive_ components and should not used together.
```html
```
❌ Or this:
Modifer class is used without corresponding base classname.
```html
```
Also consider keeping modifiers at the highest element that makes sense. This makes the component more extensible and resilient as styling needs are changed or added in the future.
✅ Do this:
```html
```
### Utility Classes
Utility classes are intended to act as global overrides for a single styling decision, e.g. alignment, show/hide, etc. BEM conventions are not followed, there is no hierarchy in utility classes and utility classes do not assume they are used with any particular block or element.
Name multi-word utility classes with hyphens `-`. Append any viewport specifications at the **end**, e.g. `hidden-mobile`.
✅ This is fine:
```css
.align-left {
text-align: left;
}
```
```html
```
## Modern CSS Features
### Container Queries
Use container queries for truly responsive components:
```css
.product-grid {
container-type: inline-size;
}
@container (min-width: 400px) {
.product-card {
display: grid;
grid-template-columns: 1fr 1fr;
}
}
```
### CSS Functions
Leverage modern CSS functions for better responsiveness:
```css
.component {
/* Fluid spacing */
padding: clamp(1rem, 4vw, 3rem);
/* Intrinsic sizing */
width: min(100%, 800px);
/* Dynamic colors */
/* color-mix isn't supported in earlier version of iOS <16.2 so limit its usage */
background: color-mix(in srgb, rgb(var(--color-primary)) 90%, white);
}
```
### Cascade Layers
For better CSS organization in complex themes:
```css
@layer reset, base, components, utilities, overrides;
@layer components {
.button {
/* Component styles here won't conflict with utilities */
}
}
```
### View Transitions
```css
@view-transition {
navigation: auto;
}
.page-content {
view-transition-name: main-content;
}
```
## Media Queries
- Default to mobile first. e.g. `min-width` queries
- Use `screen` for all media queries
### Breakpoint System
Define consistent breakpoints:
```css
/* Mobile first breakpoints */
--breakpoint-sm: 576px; /* Small devices */
--breakpoint-md: 768px; /* Medium devices */
--breakpoint-lg: 992px; /* Large devices */
--breakpoint-xl: 1200px; /* Extra large devices */
--breakpoint-2xl: 1400px; /* 2X Extra large devices */
```
### Context-Aware Queries
Use feature queries alongside media queries:
```css
@supports (display: grid) {
.product-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
}
@supports not (display: grid) {
.product-grid {
display: flex;
flex-wrap: wrap;
}
}
```
### Print Styles
Always consider print stylesheets:
```css
@media print {
.no-print {
display: none !important;
}
a[href^='http']:after {
content: ' (' attr(href) ')';
}
}
```
## CSS Nesting Rules
Nesting can make styles harder to read. Be responsible with it.
- **No `&` operator** in nested selectors
- **Never nest beyond first level** (except media queries/states)
- **Keep nesting simple** and readable
- Only use `&` when there is a direct relationship between the two selectors
- State based selectors e.g. `&:hover`, `&:focus`, `&:active`
- Modifiers that affect each other e.g. `button--integrated { &.button--text }`
- Never nest beyond the first level
- See below for exceptions
### Nesting Media Queries
Use nesting for media queries
```css
.header {
width: 100%;
@media screen and (min-width: 750px) {
width: 100px;
}
}
```
This includes when there is nothing to override, e.g.
```css
.header {
@media screen and (min-width: 750px) {
width: 100px;
}
}
```
That way, if something needs to be added later, it can just be added without needing to flip the media query to the inside.
### If-like Parent-Child Relationships
You may use nesting to help organize parent-child relationship when the parent can have **multiple states or modifiers** that affect children. In the example below, a number of child selectors need to change when the parent is the `--full-width` variant. This saves you from needing to append `parent--full-width` to each css selector.
```css
.parent {
grid-columns: var(--gap) 1fr var(--gap);
}
.child {
grid-column: 2;
}
.grand-child {
...;
}
.parent--full-screen {
grid-columns: 1fr;
.child {
grid-column: 1;
}
.grand-child {
...;
}
}
```
In cases like this, the styles that are being applied are the direct result of the parent's modifier. We can see this as a kind of if-like relationship where the logic is easier to follow if the child styles are nested inside the parent.
This is not a reason to nest multiple levels. Maintain the single level rule.
## Logical Properties
Where appropriate, use logical properties to have baseline support for Right-to-Left (RTL) languages.
Focusing on these properties:
- padding
- margin
- border
- text-align
- top, bottom, left, right
✅ Do this:
```css
.element {
padding-inline: 2rem;
padding-block: 1rem;
margin-inline: auto;
margin-block: 0;
border-inline-end: 1rem solid var(--color-background);
text-align: start;
inset: 0;
}
```
❌ Not this:
```css
.element {
padding: 1rem 2rem;
margin: 0 auto;
border-bottom: 1rem solid var(--color-background);
text-align: left;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
```
## Layout Patterns
### CSS Grid for Layouts
```css
.section-content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: var(--spacing-lg);
}
```
### Flexbox for Components
```css
.product-card {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
}
```
### Aspect Ratio for Media
```css
.product-card__image {
aspect-ratio: 4 / 3;
object-fit: cover;
}
```
## Fancy Selectors
### Using `:is()`
When giving the same styles to multiple selectors, use a comma separated list.
✅ Do this:
```css
.facets__label,
.facets__clear-all,
.clear-filter {
...;
}
```
❌ Not this:
```css
:is(.facets__label, .facets__clear-all, .clear-filter) {
...;
}
```
However, if you are giving the same styles to a parent-child relationship with different selectors, you may use `:is()`.
✅ Do this:
```css
.parent:is(.child-1, .child-2) {
...;
}
```
❌ Not this:
```css
.parent .child-1,
.parent .child-2 {
...;
}
```
✅ Do this:
```css
:is(.parent, .parent-2) .child {
...;
}
```
❌ Not this:
```css
.parent .child,
.parent-2 .child {
...;
}
```
Try to keep the same specificity for all selectors within a single `:is()` to avoid increasing the overall specificity of the selector unintentionally.
## Accessibility
### Motion and Animation
- Always respect user motion preferences
- Provide fallbacks for users who prefer reduced motion
```css
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
```
### Focus Management
- Ensure all interactive elements have visible focus indicators
- Use `:focus-visible` for better UX
```css
.button:focus-visible {
outline: 2px solid rgb(var(--color-focus));
outline-offset: 2px;
}
```
### Color and Contrast
- Maintain WCAG AA contrast ratios (4.5:1 for normal text, 3:1 for large text)
- Test with high contrast mode
- Never rely solely on color to convey information
```css
@media (prefers-color-scheme: dark) {
:root {
/* Dark theme variables */
}
}
```
## Performance Considerations
### Animation Performance
- Use `transform` and `opacity` for animations
- Avoid animating layout properties (`width`, `height`, `margin`, `padding`)
- Use `will-change` sparingly and remove after animation
```css
.product-card {
transition: transform 0.2s ease;
}
.product-card:hover {
transform: translateY(-2px); /* Better than animating top/margin */
}
/* Only use will-change during animation */
.product-card:hover {
will-change: transform;
}
.product-card:not(:hover) {
will-change: auto;
}
```
### Layout Performance
- Use `contain` property for better rendering performance
- Prefer CSS Grid and Flexbox over complex positioning
```css
.product-grid {
contain: content;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
```
## CSS Organization
### CSS Property Order
Maintain consistent property order within declarations:
```css
.component {
/* 1. Layout & Positioning */
position: relative;
display: flex;
flex-direction: column;
/* 2. Box Model */
width: 100%;
margin: 0;
padding: var(--space-md);
border: 1px solid rgb(var(--color-border));
/* 3. Typography */
font-family: var(--font-body-family);
font-size: var(--font-size-base);
/* 4. Visual */
background: rgb(var(--color-surface));
color: rgb(var(--color-text));
/* 5. Animation & Transforms */
transition: transform 0.2s ease;
}
```
## Error Prevention
### Common Pitfalls
- **Never** use `position: fixed` without considering mobile keyboards
- **Always** test with zoom up to 200%
- **Avoid** magic numbers - use variables or calc() instead
- **Remember** that `vh` units can be problematic on mobile, use `dvh` to mitage this
### Defensive CSS
Write CSS that gracefully handles edge cases:
```css
.product-card {
/* Prevent content overflow */
word-wrap: break-word;
overflow-wrap: break-word;
/* Handle long content */
min-width: 0; /* Allows flex items to shrink below content size */
/* Prevent layout shift */
aspect-ratio: 1 / 1;
/* Fallback for missing images */
background: rgb(var(--color-surface-secondary));
}
```
### Browser Support
- Test in browsers used by your audience
- Provide fallbacks for newer CSS features
- Use progressive enhancement approach
## CSS Documentation
### Commenting Standards
Use consistent commenting for better maintainability:
```css
/* =============================================================================
Component Name
============================================================================= */
/**
* Brief component description
*
* @example
*
*
Content
*
*/
.component {
/* Implementation */
}
/* Component modifiers
========================================================================== */
/**
* Modifier description
*/
.component--modifier {
/* Modifier styles */
}
/* Component elements
========================================================================== */
/**
* Element description
*/
.component__element {
/* Element styles */
}
```
## Example Component Structure
```liquid
{% stylesheet %}
.featured-collection {
--section-padding: {{ section.settings.padding | default: 60 }}px;
--bg-color: {{ section.settings.background_color | default: '#ffffff' }};
--text-color: {{ section.settings.text_color | default: '#000000' }};
padding: var(--section-padding) 0;
background-color: var(--bg-color);
color: var(--text-color);
container-type: inline-size;
}
.featured-collection__grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: var(--spacing-md);
}
@container (min-width: 768px) {
.featured-collection__grid {
grid-template-columns: repeat({{ section.settings.columns | default: 4 }}, 1fr);
}
}
@media (prefers-reduced-motion: reduce) {
.featured-collection * {
transition: none !important;
}
}
{% endstylesheet %}
```
================================================
FILE: .cursor/rules/disclosure-accessibility.mdc
================================================
---
description: Disclosure component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Disclosure Component Accessibility Standards
Ensures disclosure components follow WCAG compliance and WAI-ARIA Disclosure Pattern specifications.
name: disclosure_accessibility_standards
description: Enforce disclosure component accessibility standards and WAI-ARIA Disclosure Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Button role requirement
- pattern: "(?i)<(button|div|span)[^>]*(?:disclosure|expand|collapse)[^>]*>"
pattern_negate: "role=\"button\""
message: "Disclosure controls must have role='button' (or use native button element which has implicit role)."
# aria-expanded requirement
- pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:disclosure|expand|collapse)[^>]*>"
pattern_negate: "aria-expanded=\"(true|false)\""
message: "Disclosure controls must have aria-expanded attribute set to 'true' or 'false'."
# Missing keyboard event handlers
- pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:disclosure|expand|collapse)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Disclosure controls should handle keyboard events (Enter and Space)."
- type: suggest
message: |
**Disclosure Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **role='button':** Set on the disclosure control element (or use native button)
- **aria-expanded:** 'true' if content is visible, 'false' if hidden
- **aria-controls:** (Optional) Reference to the ID of the associated content
**DOM Structure Requirements:**
- The disclosure content MUST be a sibling to the disclosure control in the DOM
- This ensures proper content discovery and navigation for all users
- Avoid placing content in different containers or far from the control
- Maintain a logical reading order in the DOM
**Keyboard Interaction Requirements:**
- **Enter:** Toggle disclosure content visibility
- **Space:** Toggle disclosure content visibility
- **Tab:** Move focus to next focusable element
- **Shift+Tab:** Move focus to previous focusable element
**Implementation Example:**
```html
Disclosure Title
Disclosure content goes here...
Disclosure Title
Other content...
Disclosure content goes here...
```
**JavaScript Considerations:**
- Implement Enter and Space key handlers for toggling
- Update aria-expanded state when content toggles
- Use hidden attribute or CSS to show/hide content
- Consider implementing smooth transitions
**Accessibility Notes:**
- Button should be the only element inside the control
- Content MUST be a sibling to the control in the DOM
- Visual focus indicators should be clear
- Test with screen readers to ensure proper announcement
- Consider adding aria-label if the button text is not descriptive
- Maintain proper reading order for screen reader users
- Avoid complex DOM structures that could confuse navigation
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/dropdown-navigation-accessibility.mdc
================================================
---
description: Dropdown Navigation component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Dropdown Navigation Component Accessibility Standards
Ensures dropdown navigation components follow WCAG compliance and proper navigation semantics, including mobile modal patterns and disclosure controls.
name: dropdown_navigation_accessibility_standards
description: Enforce dropdown navigation component accessibility standards and proper navigation semantics
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Navigation landmark requirement
- pattern: "(?i)]*(?:navigation|menu|dropdown)[^>]*>"
pattern_negate: "(aria-label|aria-labelledby)=\"[^\"]+\""
message: "Navigation elements must have aria-label or aria-labelledby attribute for accessibility."
# Navigation list structure requirement
- pattern: "(?i)]*(?:navigation|menu|dropdown)[^>]*>"
pattern_negate: "
================================================
FILE: .cursor/rules/flip-card-accessibility.mdc
================================================
---
description: Flip Card component accessibility compliance pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Flip Card Component Accessibility Standards
Ensures flip card components follow WCAG compliance and provide proper state management for screen reader users.
name: flip_card_accessibility_standards
description: Enforce flip card component accessibility standards and proper state management
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Flip button requirement
- pattern: "(?i)<(div|section)[^>]*(?:card|flip)[^>]*>"
pattern_negate: "]*aria-pressed"
message: "Flip cards must contain a button with aria-pressed attribute to control card state."
# aria-pressed attribute requirement
- pattern: "(?i)]*(?:flip|card)[^>]*>"
pattern_negate: "aria-pressed=\"(true|false)\""
message: "Flip card buttons must have aria-pressed attribute set to 'true' or 'false'."
# Card front/back structure requirement
- pattern: "(?i)<(div|section)[^>]*(?:card|flip)[^>]*>"
pattern_negate: "(card--front|card--back|front|back)"
message: "Flip cards must have both front and back content sections for proper structure."
# Unique accessible name requirement
- pattern: "(?i)]*aria-pressed[^>]*>"
pattern_negate: "(aria-label|aria-labelledby|>.*[A-Za-z]{10,})"
message: "Flip card buttons must have unique, descriptive accessible names that reference visible card content."
# Keyboard focus indicator requirement
- pattern: "(?i)<(div|section)[^>]*(?:card|flip)[^>]*>"
pattern_negate: "(focus|:focus|focus-visible|:focus-visible)"
message: "Flip card containers should have visible keyboard focus indicators when the flip button is focused."
# Content visibility management
- pattern: "(?i)aria-pressed=\"(true|false)\""
pattern_negate: "(visibility.*hidden|display.*none|hidden)"
message: "Use aria-pressed state to control content visibility - false shows front, true shows back. Prefer visibility: hidden/visible for smooth animations."
# Missing flip button type
- pattern: "(?i)]*(?:flip|card)[^>]*>"
pattern_negate: "type=\"button\""
message: "Flip card buttons should have type='button' to prevent form submission behavior."
# Incomplete card structure
- pattern: "(?i)
]*class=\"card[^>]*>"
pattern_negate: "(card--front.*card--back|card--back.*card--front)"
message: "Flip cards must contain both front and back content sections for proper functionality."
- type: suggest
message: |
**Flip Card Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **aria-pressed:** 'false' shows front content, 'true' shows back content
- **type="button":** Prevents form submission behavior
- **Unique accessible name:** Should reference visible card content
**DOM Structure Requirements:**
- Card container with front and back content sections
- Flip button positioned between or adjacent to content sections
- Use CSS display: none to hide non-visible content
- Maintain logical reading order in the DOM
**Content Visibility Management:**
- **aria-pressed="false":** Show front content, hide back content
- **aria-pressed="true":** Show back content, hide front content
- Use CSS display property for smooth transitions
- Ensure only one side is visible at a time
**Keyboard and Focus Requirements:**
- **Enter:** Toggle card state
- **Space:** Toggle card state
- **Tab:** Move focus to next focusable element
- **Shift+Tab:** Move focus to previous focusable element
- **Focus indicator:** Should wrap the card content container
- **Hover state:** Blue border matching focus indicator for visual consistency
**Implementation Example:**
```html
Card Title
Inspiring content
More detailed content about the product
```
**CSS Implementation:**
```css
.card {
position: relative;
perspective: 1000px;
/* Focus indicator for keyboard navigation */
outline: 2px solid transparent;
outline-offset: 2px;
/* Visual affordance for clickable card */
cursor: pointer;
}
.card:focus-within {
outline-color: #0056b3;
outline-width: 3px;
}
.card:hover {
outline-color: #0056b3;
outline-width: 3px;
}
.card--front,
.card--back {
transition: opacity 0.3s ease-in-out, transform 0.3s ease-in-out, visibility 0.3s ease-in-out;
}
/* Show front by default */
.card--front {
visibility: visible;
opacity: 1;
transform: rotateY(0deg);
}
.card--back {
visibility: hidden;
opacity: 0;
transform: rotateY(180deg);
}
/* Show back when aria-pressed="true" */
.card[data-pressed="true"] .card--front {
visibility: hidden;
opacity: 0;
transform: rotateY(-180deg);
}
.card[data-pressed="true"] .card--back {
visibility: visible;
opacity: 1;
transform: rotateY(0deg);
}
.flip-button {
position: absolute;
top: 1rem;
right: 1rem;
background: #0056b3;
color: #ffffff;
border: none;
width: 40px;
height: 40px;
border-radius: 50%;
cursor: pointer;
transition: all 0.2s ease;
z-index: 10;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
line-height: 1;
}
.flip-button:hover {
background: #004085;
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0, 86, 179, 0.3);
}
.flip-button:focus {
outline: 3px solid #ffd700;
outline-offset: 2px;
background: #004085;
}
.flip-button:active {
transform: scale(0.95);
}
/* Button icon states */
.flip-button::before {
content: "↻"; /* Circular arrow icon for front state */
transition: all 0.3s ease;
}
/* Show X icon when back is visible */
.card[data-pressed="true"] .flip-button::before {
content: "×"; /* X icon for back state */
font-size: 24px;
font-weight: bold;
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.card--front,
.card--back {
transition: none;
}
.flip-button {
transition: none;
}
.flip-button:hover {
transform: none;
box-shadow: none;
}
.flip-button:active {
transform: none;
}
.card {
transition: none;
}
}
```
**JavaScript State Management:**
```javascript
const flipCards = document.querySelectorAll('.card');
flipCards.forEach(card => {
const button = card.querySelector('.flip-button');
const front = card.querySelector('.card--front');
const back = card.querySelector('.card--back');
function toggleCard() {
const isPressed = button.getAttribute('aria-pressed') === 'true';
const newState = !isPressed;
// Update ARIA state
button.setAttribute('aria-pressed', newState);
// Update card data attribute for CSS
card.setAttribute('data-pressed', newState);
// Announce state change to screen readers
const announcement = newState ? 'Showing back of card' : 'Showing front of card';
announceToScreenReader(announcement);
}
// Button click handler
button.addEventListener('click', (event) => {
event.stopPropagation(); // Prevent card click when button is clicked
toggleCard();
});
// Card container click handler for mouse/touch users
card.addEventListener('click', toggleCard);
// Keyboard handler
button.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleCard();
}
});
});
// Screen reader announcement helper
function announceToScreenReader(message) {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = message;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
```
**Accessibility Guidelines:**
**Button Requirements:**
- Must have type="button" to prevent form submission
- aria-pressed attribute must be present and toggle between "true" and "false"
- Accessible name should reference visible card content via aria-label
- Should handle both click and keyboard events
- Position in top-right corner for intuitive placement
- Use dynamic icons: ↻ (arrow) for front state, × (X) for back state
**Card Container Interaction:**
- **Mouse/Touch Support:** Allow clicking anywhere on card to flip
- **Visual Affordance:** Use cursor: pointer to indicate clickable area
- **Event Handling:** Prevent conflicts between card and button clicks
- **Accessibility Maintained:** All keyboard and screen reader functionality preserved
**Content Structure:**
- Front and back content must be present
- Only one side visible at a time
- Use semantic HTML for content (headings, paragraphs, images)
- Maintain logical reading order
- Use `visibility: hidden/visible` for smooth animations while maintaining accessibility
**Focus Management:**
- Focus indicator should wrap the entire card when button is focused
- Use :focus-within CSS pseudo-class for container focus
- Ensure focus indicator has sufficient contrast (3:1 minimum)
- Maintain focus order during state changes
**Screen Reader Support:**
- aria-pressed state announces current card side
- Consider adding aria-live region for state changes
- Ensure content is properly labeled and described
- Test with screen readers to verify announcements
**Animation Considerations:**
- Use CSS transitions for smooth flipping effects
- Ensure animations don't interfere with accessibility
- **Always implement prefers-reduced-motion media query**
- Remove all animations, transforms, and transitions when reduced motion is preferred
- Maintain content visibility during transitions
- Respect user's motion sensitivity preferences
**Button Design Best Practices:**
- **Positioning**: Place in top-right corner for intuitive access
- **Shape**: Use circular design for modern, clean appearance
- **Icons**: Implement dynamic icons that change with card state
- Front state: ↻ (circular arrow) indicating "click to flip"
- Back state: × (X) indicating "click to return"
- **Visual Feedback**: Include hover, focus, and active states
- **Accessibility**: Maintain aria-label for screen reader context
- **Responsive**: Scale appropriately for different card sizes
**Testing Checklist:**
- Verify aria-pressed toggles correctly
- Test keyboard navigation (Enter, Space, Tab)
- Check focus indicator visibility and contrast
- Validate screen reader announcements
- Test content visibility changes
- Verify accessible names are descriptive via aria-label
- Check that only one side is visible at a time
- Verify button positioning in top-right corner
- Test dynamic icon changes (↻ to ×) with state changes
- Ensure button remains accessible in all card states
- Validate responsive button sizing for different card layouts
- Test card container click functionality (anywhere on card)
- Verify hover states show blue border matching focus indicator
- Check cursor pointer appears on card hover
- Ensure no conflicts between card and button click events
- Test reduced motion preference support (prefers-reduced-motion: reduce)
- Verify all animations are disabled when reduced motion is preferred
metadata:
priority: high
version: 1.0
================================================
FILE: .cursor/rules/focus-order-and-styles-accessibility.mdc
================================================
---
description: Focus order and focus styles accessibility standards per WCAG 2.4.7 Focus Visible, 1.4.11 Non-Text Contrast, 2.4.13 Focus Appearance, and 2.4.11 Focus Not Obscured requirements
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid, *.css, *.scss, *.sass, *.less
alwaysApply: true
---
# Focus Order and Focus Styles Accessibility Standards
Ensures proper focus order, tabindex usage, and focus indicators following WCAG 2.4.7 Focus Visible, 1.4.11 Non-Text Contrast, 2.4.13 Focus Appearance, and 2.4.11 Focus Not Obscured requirements.
name: focus_order_and_styles_accessibility_standards
description: Enforce focus order and focus styles accessibility standards per WCAG requirements
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts|css|scss|sass|less)$"
actions:
- type: enforce
conditions:
# Positive tabindex values (should not be used)
- pattern: "tabindex=\"[1-9]\""
message: "Positive tabindex values create illogical focus order. Use DOM order instead or tabindex=\"0\" for custom focusable elements."
# Missing focus styles (outline: 0 or outline: none)
- pattern: "outline:\\s*0|outline:\\s*none"
message: "Focus styles should not be removed. Use custom focus indicators that meet WCAG contrast requirements."
# Focus styles with insufficient contrast (light colors)
- pattern: "outline.*#[89abcdefABCDEF]{6}|outline.*#[cdefCDEF]{3,6}"
message: "Light focus outline colors may not meet 3:1 contrast ratio requirement for UI component identification."
# Missing focus-visible implementation
- pattern: ":focus\\s*\\{"
pattern_negate: ":focus-visible|:focus:not\\(:focus-visible\\)"
message: "Consider implementing :focus-visible for better keyboard-only focus indication."
# Focus styles that may be obscured
- pattern: "outline-offset:\\s*-?0\\.?0*px|outline-offset:\\s*0"
message: "Consider using positive outline-offset to prevent focus indicators from being obscured by adjacent elements."
# Missing forced-colors media query for Windows High Contrast
- pattern: "@media\\s*\\(forced-colors:\\s*active\\)"
pattern_negate: "outline.*transparent"
message: "Windows High Contrast Mode requires transparent outline for native focus appearance."
# Custom focusable elements without proper tabindex
- pattern: "<(div|span|button)[^>]*onclick|onkeydown|onkeypress"
pattern_negate: "tabindex=\"[0-9]\"|role=\"button\"|role=\"link\""
message: "Custom interactive elements should have tabindex=\"0\" or appropriate ARIA role for keyboard accessibility."
# Focus styles with insufficient area
- pattern: "outline-width:\\s*1px|outline-width:\\s*0\\.1rem"
message: "Thin focus outlines may not meet WCAG 2.4.13 Focus Appearance requirements for minimum area."
# Focus styles that blend with background
- pattern: "outline.*rgba\\([^)]*0\\.1[^)]*\\)|outline.*rgba\\([^)]*0\\.2[^)]*\\)"
message: "Very transparent focus outlines may not provide sufficient contrast for visibility."
# Missing focus styles on interactive elements
- pattern: "<(button|a|input|select|textarea)[^>]*>"
pattern_negate: ":focus|:focus-visible|tabindex"
message: "Interactive elements should have visible focus styles for keyboard navigation accessibility."
# Dynamic content removal without focus management
- pattern: "\\.remove\\(\\)|removeChild|innerHTML\\s*="
pattern_negate: "focus\\(|focus\\(\\)"
message: "When removing dynamic content, ensure proper focus management by restoring focus to a logical location."
- type: suggest
message: |
**WCAG Focus Order and Focus Styles Requirements:**
**Focus Order Requirements:**
**1. Logical DOM Order:**
- **Default:** Focus order follows DOM element order
- **Navigation:** Tab key moves forward, Shift+Tab moves backward
- **Avoid:** Positive tabindex values (1, 2, 3, etc.)
**2. Tabindex Usage:**
```html
First ButtonSecond ButtonThird Button
.custom-button:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
background-color: #e7f3ff;
}
```
**3. Skip Links and Focus Management:**
```html
Skip to main content
Main Content
.skip-link {
position: absolute;
top: -40px;
left: 6px;
background: #0056b3;
color: #ffffff;
padding: 8px;
text-decoration: none;
z-index: 1000;
}
.skip-link:focus {
top: 6px;
outline: 2px solid #ffffff;
outline-offset: 2px;
}
```
**Focus Styles Guidelines:**
**1. Contrast Requirements:**
- **Minimum:** 3:1 contrast ratio for focus indicators
- **Recommended:** 4.5:1 or higher for better visibility
- **Test:** Against adjacent colors and backgrounds
**2. Size and Visibility:**
- **Outline width:** Minimum 2px for visibility
- **Outline offset:** Use positive values to prevent overlap
- **Area:** Focus indicator should be clearly visible
**3. Color Selection:**
```css
/* High contrast focus colors */
:focus-visible {
outline: 2px solid #0056b3; /* Blue - high contrast */
outline-offset: 2px;
}
/* Alternative high contrast colors */
:focus-visible {
outline: 2px solid #dc3545; /* Red - high contrast */
outline-offset: 2px;
}
:focus-visible {
outline: 2px solid #198754; /* Green - high contrast */
outline-offset: 2px;
}
```
**4. Focus Not Obscured:**
```css
/* Prevent focus indicator overlap */
button:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 3px; /* Space between element and outline */
}
/* Alternative: Use box-shadow for non-overlapping focus */
button:focus-visible {
outline: none;
box-shadow: 0 0 0 2px #ffffff, 0 0 0 4px #0056b3;
}
```
**5. Dynamic Content Focus Management:**
```javascript
// Focus management best practices
class FocusManager {
constructor() {
this.focusHistory = [];
this.currentFocus = null;
}
// Save focus before making changes
saveFocus() {
this.currentFocus = document.activeElement;
this.focusHistory.push(this.currentFocus);
}
// Restore focus to previous location
restoreFocus() {
if (this.focusHistory.length > 0) {
const previousFocus = this.focusHistory.pop();
if (previousFocus && document.contains(previousFocus)) {
previousFocus.focus();
}
}
}
// Focus first interactive element in new content
focusNewContent(container) {
const focusableElements = container.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElements.length > 0) {
focusableElements[0].focus();
}
}
// Find logical focus target when content is removed
findLogicalFocusTarget(removedElement, container) {
// Try to focus next sibling element
const nextSibling = removedElement.nextElementSibling;
if (nextSibling) {
const focusableElement = nextSibling.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElement) {
return focusableElement;
}
}
// Try to focus previous sibling element
const prevSibling = removedElement.previousElementSibling;
if (prevSibling) {
const focusableElement = prevSibling.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusableElement) {
return focusableElement;
}
}
// Fallback to container or trigger button
return container.querySelector('button, [href], input') ||
document.querySelector('[aria-haspopup="dialog"]');
}
}
// Usage example
const focusManager = new FocusManager();
function addContent() {
focusManager.saveFocus();
// Add new content
const newContent = createNewContent();
container.appendChild(newContent);
// Focus first interactive element
focusManager.focusNewContent(newContent);
}
function removeContent(element) {
const container = element.parentElement;
// Find logical focus target before removing
const focusTarget = focusManager.findLogicalFocusTarget(element, container);
// Remove the element
element.remove();
// Focus the logical target
if (focusTarget) {
focusTarget.focus();
}
}
```
**Testing and Validation:**
**1. Keyboard Navigation Testing:**
- Navigate using Tab and Shift+Tab
- Verify focus order is logical
- Check that focus indicators are visible
- Test with different focus styles
**2. Contrast Testing:**
- Use browser dev tools for contrast ratios
- Test against different backgrounds
- Verify 3:1 minimum contrast requirement
- Test with color blindness simulators
**3. Focus Visibility Testing:**
- Test with screen readers
- Verify focus indicators are not obscured
- Check focus styles in different themes
- Test Windows High Contrast Mode
**4. Dynamic Content Focus Testing:**
- Test focus management when adding new content
- Verify focus moves to first interactive element in new content
- Test focus restoration when removing content
- Ensure focus returns to logical location
- Test focus management with multiple dynamic elements
- Verify focus history is maintained correctly
**Common Mistakes to Avoid:**
**1. Focus Order Issues:**
- Using positive tabindex values
- Skipping focusable elements
- Illogical DOM structure
- Missing focusable elements
**2. Focus Style Problems:**
- Removing focus styles with `outline: none`
- Insufficient contrast ratios
- Focus indicators that are too small
- Focus styles that blend with background
**3. Implementation Issues:**
- Missing focus-visible implementation
- Not testing with keyboard navigation
- Ignoring Windows High Contrast Mode
- Focus indicators that are obscured
**4. Accessibility Violations:**
- No visible focus indicators
- Focus order that doesn't match reading order
- Custom elements without proper focus management
- Missing keyboard event handlers
**Advanced Focus Management:**
**1. Programmatic Focus Control:**
```javascript
// Focus management for modals
function openModal() {
const modal = document.getElementById('modal');
const closeButton = document.getElementById('close-modal');
modal.style.display = 'block';
closeButton.focus(); // Focus close button when modal opens
}
// Trap focus within modal
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
// Handle Tab key
document.addEventListener('keydown', function(e) {
if (e.key === 'Tab') {
if (e.shiftKey) {
if (document.activeElement === firstElement) {
lastElement.focus();
e.preventDefault();
}
} else {
if (document.activeElement === lastElement) {
firstElement.focus();
e.preventDefault();
}
}
}
});
}
```
**2. Dynamic Focus Management:**
```javascript
// Focus restoration after dynamic content
function loadContent() {
const container = document.getElementById('content');
const previousFocus = document.activeElement;
// Load new content
container.innerHTML = newContent;
// Restore focus or set to first focusable element
const firstFocusable = container.querySelector(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (firstFocusable) {
firstFocusable.focus();
} else if (previousFocus) {
previousFocus.focus();
}
}
```
**3. Focus Indicators for Complex Components:**
```css
/* Multi-state focus indicators */
.accordion-item:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
.accordion-item[aria-expanded="true"]:focus-visible {
outline-color: #198754; /* Different color for expanded state */
}
/* Focus indicators for different interaction states */
.interactive-element:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
.interactive-element:hover:focus-visible {
outline-color: #004085; /* Darker on hover + focus */
}
.interactive-element:active:focus-visible {
outline-color: #002752; /* Even darker when active */
}
```
**4. Dynamic Content Focus Management:**
```javascript
// When adding content: focus first interactive element
function addDynamicContent() {
const container = document.getElementById('content');
const newElement = document.createElement('div');
newElement.innerHTML = `
New Content
Action Button
`;
container.appendChild(newElement);
// Focus the first focusable element in new content
const firstFocusable = newElement.querySelector('button');
if (firstFocusable) {
firstFocusable.focus();
}
}
// When removing content: restore focus to logical location
function removeDynamicContent(element) {
const triggerButton = document.querySelector('[onclick="addDynamicContent()"]');
// Store reference to element being removed
const removedElement = element;
// Remove the element
element.remove();
// Restore focus to the trigger button
if (triggerButton) {
triggerButton.focus();
}
}
// Advanced focus management for multiple elements
function removeSpecificElement(element, elementType) {
const container = element.parentElement;
const triggerButton = document.querySelector(`[onclick="add${elementType}()"]`);
// Find the next logical focus target
let nextFocusTarget = triggerButton;
// If there are other elements, focus the next one
const remainingElements = container.querySelectorAll(`.${elementType.toLowerCase()}`);
if (remainingElements.length > 0) {
const targetElement = remainingElements[0];
const focusableElement = targetElement.querySelector('button, a, input');
if (focusableElement) {
nextFocusTarget = focusableElement;
}
}
// Remove the element
element.remove();
// Set focus to the appropriate target
nextFocusTarget.focus();
}
```
metadata:
priority: high
version: 1.0
description:
globs:
alwaysApply: false
---
================================================
FILE: .cursor/rules/form-accessibility.mdc
================================================
---
description: Form component accessibility standards and WCAG compliance for form inputs, labels, instructions, and error handling
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: true
---
# Form Accessibility Standards
Ensures form components follow WCAG compliance and provide proper accessibility for all users including screen reader users and keyboard-only users.
name: form_accessibility_standards
description: Enforce form component accessibility standards and WCAG compliance for form inputs, labels, instructions, and error handling
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Missing label association for form inputs
- pattern: "(?i)<(input|textarea|select)[^>]*>"
pattern_negate: "(