Showing preview only (4,891K chars total). Download the full file or copy to clipboard to get everything.
Repository: Shopify/horizon
Branch: main
Commit: df79657df860
Files: 425
Total size: 4.5 MB
Directory structure:
gitextract_bgoglxxq/
├── .cursor/
│ └── rules/
│ ├── accordion-accessibility.mdc
│ ├── animation-accessibility.mdc
│ ├── assets.mdc
│ ├── blocks.mdc
│ ├── breadcrumb-accessibility.mdc
│ ├── carousel-accessibility.mdc
│ ├── cart-drawer-accessibility.mdc
│ ├── chat-window-accessibility.mdc
│ ├── color-contrast-accessibility.mdc
│ ├── color-swatch-accessibility.mdc
│ ├── combobox-accessibility.mdc
│ ├── css-standards.mdc
│ ├── disclosure-accessibility.mdc
│ ├── dropdown-navigation-accessibility.mdc
│ ├── examples/
│ │ ├── block-example-group.liquid
│ │ ├── block-example-text.liquid
│ │ ├── section-example.liquid
│ │ └── snippet-example.liquid
│ ├── flip-card-accessibility.mdc
│ ├── focus-order-and-styles-accessibility.mdc
│ ├── form-accessibility.mdc
│ ├── global-accessibility-standards.mdc
│ ├── heading-accessibility.mdc
│ ├── html-standards.mdc
│ ├── image-alt-text-accessibility.mdc
│ ├── javascript-standards.mdc
│ ├── landmark-accessibility.mdc
│ ├── liquid.mdc
│ ├── locales.mdc
│ ├── localization.mdc
│ ├── mobile-accessibility-standards.mdc
│ ├── modal-accessibility.mdc
│ ├── product-card-accessibility.mdc
│ ├── product-filter-accessibility.mdc
│ ├── product-media-gallery-accessibility.mdc
│ ├── prompts-and-references.mdc
│ ├── sale-price-accessibility.mdc
│ ├── schemas.mdc
│ ├── sections.mdc
│ ├── slider-accessibility.mdc
│ ├── snippets.mdc
│ ├── switch-accessibility.mdc
│ ├── tab-accessibility.mdc
│ ├── table-accessibility.mdc
│ ├── templates.mdc
│ ├── theme-settings.mdc
│ └── tooltip-accessibility.mdc
├── LICENSE.md
├── README.md
├── assets/
│ ├── accordion-custom.js
│ ├── anchored-popover.js
│ ├── announcement-bar.js
│ ├── auto-close-details.js
│ ├── base.css
│ ├── blog-posts-list.js
│ ├── cart-discount.js
│ ├── cart-drawer.js
│ ├── cart-icon.js
│ ├── cart-note.js
│ ├── collection-links.js
│ ├── comparison-slider.js
│ ├── component-cart-items.js
│ ├── component-cart-quantity-selector.js
│ ├── component-quantity-selector.js
│ ├── component.js
│ ├── copy-to-clipboard.js
│ ├── dialog.js
│ ├── drag-zoom-wrapper.js
│ ├── events.js
│ ├── facets.js
│ ├── floating-panel.js
│ ├── fly-to-cart.js
│ ├── focus.js
│ ├── gift-card-recipient-form.js
│ ├── global.d.ts
│ ├── header-drawer.js
│ ├── header-menu.js
│ ├── header.js
│ ├── jsconfig.json
│ ├── jumbo-text.js
│ ├── layered-slideshow.js
│ ├── local-pickup.js
│ ├── localization.js
│ ├── marquee.js
│ ├── media-gallery.js
│ ├── media.js
│ ├── money-formatting.js
│ ├── morph.js
│ ├── overflow-list.css
│ ├── overflow-list.js
│ ├── paginated-list-aspect-ratio.js
│ ├── paginated-list.js
│ ├── performance.js
│ ├── popover-polyfill.js
│ ├── predictive-search.js
│ ├── price-per-item.js
│ ├── product-card.js
│ ├── product-custom-property.js
│ ├── product-form.js
│ ├── product-hotspot.js
│ ├── product-inventory.js
│ ├── product-price.js
│ ├── product-recommendations.js
│ ├── product-sku.js
│ ├── product-title-truncation.js
│ ├── qr-code-generator.js
│ ├── qr-code-image.js
│ ├── quick-add.js
│ ├── quick-order-list.js
│ ├── recently-viewed-products.js
│ ├── results-list.js
│ ├── rte-formatter.js
│ ├── scrolling.js
│ ├── search-page-input.js
│ ├── section-hydration.js
│ ├── section-renderer.js
│ ├── show-more.js
│ ├── slideshow.js
│ ├── sticky-add-to-cart.js
│ ├── template-giftcard.css
│ ├── theme-editor.js
│ ├── utilities.js
│ ├── variant-picker.js
│ ├── video-background.js
│ ├── view-transitions.js
│ ├── volume-pricing-info.js
│ ├── volume-pricing.js
│ └── zoom-dialog.js
├── blocks/
│ ├── _accordion-row.liquid
│ ├── _announcement.liquid
│ ├── _blog-post-card.liquid
│ ├── _blog-post-content.liquid
│ ├── _blog-post-description.liquid
│ ├── _blog-post-featured-image.liquid
│ ├── _blog-post-image.liquid
│ ├── _blog-post-info-text.liquid
│ ├── _card.liquid
│ ├── _carousel-content.liquid
│ ├── _cart-products.liquid
│ ├── _cart-summary.liquid
│ ├── _cart-title.liquid
│ ├── _collection-card-image.liquid
│ ├── _collection-card.liquid
│ ├── _collection-image.liquid
│ ├── _collection-info.liquid
│ ├── _collection-link.liquid
│ ├── _content-without-appearance.liquid
│ ├── _content.liquid
│ ├── _divider.liquid
│ ├── _featured-blog-posts-card.liquid
│ ├── _featured-blog-posts-image.liquid
│ ├── _featured-blog-posts-title.liquid
│ ├── _featured-product-gallery.liquid
│ ├── _featured-product-information-carousel.liquid
│ ├── _featured-product-price.liquid
│ ├── _featured-product.liquid
│ ├── _footer-social-icons.liquid
│ ├── _header-logo.liquid
│ ├── _header-menu.liquid
│ ├── _heading.liquid
│ ├── _hotspot-product.liquid
│ ├── _image.liquid
│ ├── _inline-collection-title.liquid
│ ├── _inline-text.liquid
│ ├── _layered-slide.liquid
│ ├── _marquee.liquid
│ ├── _media-without-appearance.liquid
│ ├── _media.liquid
│ ├── _product-card-gallery.liquid
│ ├── _product-card-group.liquid
│ ├── _product-card.liquid
│ ├── _product-details.liquid
│ ├── _product-list-button.liquid
│ ├── _product-list-content.liquid
│ ├── _product-list-text.liquid
│ ├── _product-media-gallery.liquid
│ ├── _search-input.liquid
│ ├── _slide.liquid
│ ├── _social-link.liquid
│ ├── accelerated-checkout.liquid
│ ├── accordion.liquid
│ ├── add-to-cart.liquid
│ ├── button.liquid
│ ├── buy-buttons.liquid
│ ├── collection-card.liquid
│ ├── collection-title.liquid
│ ├── comparison-slider.liquid
│ ├── contact-form-submit-button.liquid
│ ├── contact-form.liquid
│ ├── custom-liquid.liquid
│ ├── email-signup.liquid
│ ├── featured-collection.liquid
│ ├── filters.liquid
│ ├── follow-on-shop.liquid
│ ├── footer-copyright.liquid
│ ├── footer-policy-list.liquid
│ ├── group.liquid
│ ├── icon.liquid
│ ├── image.liquid
│ ├── jumbo-text.liquid
│ ├── logo.liquid
│ ├── menu.liquid
│ ├── page-content.liquid
│ ├── page.liquid
│ ├── payment-icons.liquid
│ ├── popup-link.liquid
│ ├── price.liquid
│ ├── product-card.liquid
│ ├── product-custom-property.liquid
│ ├── product-description.liquid
│ ├── product-inventory.liquid
│ ├── product-recommendations.liquid
│ ├── product-title.liquid
│ ├── quantity.liquid
│ ├── review.liquid
│ ├── sku.liquid
│ ├── social-links.liquid
│ ├── spacer.liquid
│ ├── swatches.liquid
│ ├── text.liquid
│ ├── variant-picker.liquid
│ └── video.liquid
├── config/
│ ├── settings_data.json
│ └── settings_schema.json
├── layout/
│ ├── password.liquid
│ └── theme.liquid
├── locales/
│ ├── bg.json
│ ├── cs.json
│ ├── cs.schema.json
│ ├── da.json
│ ├── da.schema.json
│ ├── de.json
│ ├── de.schema.json
│ ├── el.json
│ ├── en.default.json
│ ├── en.default.schema.json
│ ├── es.json
│ ├── es.schema.json
│ ├── fi.json
│ ├── fi.schema.json
│ ├── fr.json
│ ├── fr.schema.json
│ ├── hr.json
│ ├── hu.json
│ ├── id.json
│ ├── it.json
│ ├── it.schema.json
│ ├── ja.json
│ ├── ja.schema.json
│ ├── ko.json
│ ├── ko.schema.json
│ ├── lt.json
│ ├── nb.json
│ ├── nb.schema.json
│ ├── nl.json
│ ├── nl.schema.json
│ ├── pl.json
│ ├── pl.schema.json
│ ├── pt-BR.json
│ ├── pt-BR.schema.json
│ ├── pt-PT.json
│ ├── pt-PT.schema.json
│ ├── ro.json
│ ├── ru.json
│ ├── sk.json
│ ├── sl.json
│ ├── sv.json
│ ├── sv.schema.json
│ ├── th.json
│ ├── th.schema.json
│ ├── tr.json
│ ├── tr.schema.json
│ ├── vi.json
│ ├── zh-CN.json
│ ├── zh-CN.schema.json
│ ├── zh-TW.json
│ └── zh-TW.schema.json
├── release-notes.md
├── sections/
│ ├── _blocks.liquid
│ ├── carousel.liquid
│ ├── collection-links.liquid
│ ├── collection-list.liquid
│ ├── custom-liquid.liquid
│ ├── divider.liquid
│ ├── featured-blog-posts.liquid
│ ├── featured-product-information.liquid
│ ├── featured-product.liquid
│ ├── footer-group.json
│ ├── footer-utilities.liquid
│ ├── footer.liquid
│ ├── header-announcements.liquid
│ ├── header-group.json
│ ├── header.liquid
│ ├── hero.liquid
│ ├── layered-slideshow.liquid
│ ├── logo.liquid
│ ├── main-404.liquid
│ ├── main-blog-post.liquid
│ ├── main-blog.liquid
│ ├── main-cart.liquid
│ ├── main-collection-list.liquid
│ ├── main-collection.liquid
│ ├── main-page.liquid
│ ├── marquee.liquid
│ ├── media-with-content.liquid
│ ├── password-footer.liquid
│ ├── password.liquid
│ ├── predictive-search-empty.liquid
│ ├── predictive-search.liquid
│ ├── product-hotspots.liquid
│ ├── product-information.liquid
│ ├── product-list.liquid
│ ├── product-recommendations.liquid
│ ├── quick-order-list.liquid
│ ├── search-header.liquid
│ ├── search-results.liquid
│ ├── section-rendering-product-card.liquid
│ ├── section.liquid
│ └── slideshow.liquid
├── snippets/
│ ├── add-to-cart-button.liquid
│ ├── background-media.liquid
│ ├── bento-grid.liquid
│ ├── blog-comment-form.liquid
│ ├── border-override.liquid
│ ├── button.liquid
│ ├── card-gallery.liquid
│ ├── cart-bubble.liquid
│ ├── cart-products.liquid
│ ├── cart-summary.liquid
│ ├── checkbox.liquid
│ ├── collection-card.liquid
│ ├── color-schemes.liquid
│ ├── divider.liquid
│ ├── editorial-blog-grid.liquid
│ ├── editorial-collection-grid.liquid
│ ├── editorial-product-grid.liquid
│ ├── filter-remove-buttons.liquid
│ ├── fonts.liquid
│ ├── format-price.liquid
│ ├── gap-style.liquid
│ ├── gift-card-recipient-form.liquid
│ ├── grid-density-controls.liquid
│ ├── group.liquid
│ ├── header-actions.liquid
│ ├── header-drawer.liquid
│ ├── header-row.liquid
│ ├── icon-or-image.liquid
│ ├── icon.liquid
│ ├── image.liquid
│ ├── jumbo-text.liquid
│ ├── layout-panel-style.liquid
│ ├── link-featured-image.liquid
│ ├── list-filter.liquid
│ ├── localization-form.liquid
│ ├── media.liquid
│ ├── mega-menu-list.liquid
│ ├── menu-font-styles.liquid
│ ├── meta-tags.liquid
│ ├── overflow-list.liquid
│ ├── overlay.liquid
│ ├── pagination-controls.liquid
│ ├── password-layout-styles.liquid
│ ├── predictive-search-empty-state.liquid
│ ├── predictive-search-products-list.liquid
│ ├── predictive-search-resource-carousel.liquid
│ ├── price-filter.liquid
│ ├── price.liquid
│ ├── product-card.liquid
│ ├── product-grid.liquid
│ ├── product-information-content.liquid
│ ├── product-media-gallery-content.liquid
│ ├── product-media.liquid
│ ├── quantity-selector.liquid
│ ├── quick-add-modal.liquid
│ ├── quick-add.liquid
│ ├── resource-card.liquid
│ ├── resource-image.liquid
│ ├── resource-list-carousel.liquid
│ ├── resource-list.liquid
│ ├── scripts.liquid
│ ├── search-modal.liquid
│ ├── search.liquid
│ ├── section.liquid
│ ├── size-style.liquid
│ ├── skip-to-content-link.liquid
│ ├── sku.liquid
│ ├── slideshow-arrow.liquid
│ ├── slideshow-arrows.liquid
│ ├── slideshow-controls.liquid
│ ├── slideshow-slide.liquid
│ ├── slideshow.liquid
│ ├── sorting.liquid
│ ├── spacing-padding.liquid
│ ├── spacing-style.liquid
│ ├── strikethrough-variant.liquid
│ ├── stylesheets.liquid
│ ├── submenu-font-styles.liquid
│ ├── swatch.liquid
│ ├── tax-info.liquid
│ ├── text.liquid
│ ├── theme-editor.liquid
│ ├── theme-styles-variables.liquid
│ ├── typography-style.liquid
│ ├── unit-price.liquid
│ ├── util-autofill-img-size-attr.liquid
│ ├── util-mega-menu-img-sizes-attr.liquid
│ ├── util-product-grid-card-size.liquid
│ ├── util-product-media-sizes-attr.liquid
│ ├── variant-main-picker.liquid
│ ├── variant-swatches.liquid
│ ├── video.liquid
│ └── volume-pricing-info.liquid
└── templates/
├── 404.json
├── article.json
├── blog.json
├── cart.json
├── collection.json
├── gift_card.liquid
├── index.json
├── list-collections.json
├── page.contact.json
├── page.json
├── password.json
├── product.json
└── search.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .cursor/rules/accordion-accessibility.mdc
================================================
---
description: Accordion component accessibility compliance and WAI-ARIA Accordion Pattern
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid
alwaysApply: false
---
# Accordion Component Accessibility Standards
Ensures accordion components follow WCAG compliance and WAI-ARIA Accordion Pattern specifications.
<rule>
name: accordion_accessibility_standards
description: Enforce accordion component accessibility standards and WAI-ARIA Accordion Pattern compliance
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts)$"
actions:
- type: enforce
conditions:
# Accordion header button role requirement
- pattern: "(?i)<button[^>]_(?:accordion|expand|collapse)[^>]_>"
pattern_negate: "role=\"button\""
message: "Accordion header buttons should have role='button' (or use native button element which has implicit role)."
# Accordion header missing aria-expanded
- pattern: "(?i)<button[^>]_(?:accordion|expand|collapse)[^>]_>"
pattern_negate: "aria-expanded=\"(true|false)\""
message: "Accordion header buttons must have aria-expanded attribute set to 'true' or 'false'."
# Accordion header missing aria-controls
- pattern: "(?i)<button[^>]_(?:accordion|expand|collapse)[^>]_>"
pattern_negate: "aria-controls=\"[^\"]+\""
message: "Accordion header buttons must have aria-controls attribute referencing the ID of the associated panel."
# Heading wrapper missing role
- pattern: "(?i)<(div|section)[^>]*(?:accordion.*header|header._accordion)[^>]_>"
pattern_negate: "role=\"heading\""
message: "Accordion header wrappers should have role='heading' or use native heading elements (h1-h6)."
# Heading role missing aria-level
- pattern: "(?i)<[^>]_role=\"heading\"[^>]_>"
pattern_negate: "aria-level=\"[1-6]\""
message: "Elements with role='heading' must have aria-level attribute set to appropriate level (1-6)."
# Panel missing proper identification
- pattern: "(?i)<(div|section)[^>]*(?:accordion.*panel|panel._accordion)[^>]_>"
pattern_negate: "id=\"[^\"]+\""
message: "Accordion panels must have unique ID attributes for aria-controls reference."
# Panel with region role missing aria-labelledby
- pattern: "(?i)<[^>]_role=\"region\"[^>]_>"
pattern_negate: "aria-labelledby=\"[^\"]+\""
message: "Accordion panels with role='region' must have aria-labelledby referencing the heading element."
# Missing keyboard event handlers
- pattern: "(?i)<button[^>]_(?:accordion|expand|collapse)[^>]_>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Accordion header buttons should handle keyboard events (Enter, Space, optionally Arrow keys)."
# Missing Escape key support for accordion content
- pattern: "(?i)<(div|section)[^>]*(?:accordion.*panel|panel._accordion)[^>]_>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Accordion panels should handle Escape key to close panel and return focus to header."
- type: suggest
message: |
**Accordion Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **role='button':** Set on accordion header elements (or use native button)
- **role='heading':** Set on accordion header container with aria-level
- **aria-expanded:** 'true' if panel is visible, 'false' if collapsed
- **aria-controls:** Reference to the ID of the associated panel content
- **aria-level:** Appropriate heading level (1-6) for information architecture
- **aria-disabled:** 'true' if panel cannot be collapsed (optional)
**Optional ARIA Attributes:**
- **role='region':** On panel content containers (avoid with >6 panels)
- **aria-labelledby:** On panels with role='region', referencing the heading element
**Keyboard Interaction Requirements:**
- **Enter/Space:** Toggle panel expansion/collapse
- **Tab/Shift+Tab:** Move through all focusable elements in page order
- **Down/Up Arrow:** (Optional) Navigate between accordion headers
- **Home/End:** (Optional) Jump to first/last accordion header
- **Escape:** Close open panel and return focus to header button
**Structure Requirements:**
- Header button must be the only element inside heading container
- Each panel must have unique ID for aria-controls reference
- Use native heading elements (h1-h6) when possible instead of role='heading'
- Avoid role='region' on panels when many accordions exist (>6 panels)
**Implementation Patterns:**
**Single Accordion Item:**
```html
<div class="accordion-item">
<h3
role="heading"
aria-level="3"
id="header-1"
>
<button
aria-expanded="false"
aria-controls="panel-1"
>
Section Title
</button>
</h3>
<div
id="panel-1"
role="region"
aria-labelledby="header-1"
hidden
>
<p>Panel content...</p>
</div>
</div>
```
**JavaScript for Accordion with Escape Support:**
```javascript
function toggleAccordion(button) {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
const panel = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', !isExpanded);
panel.hidden = isExpanded;
if (!isExpanded) {
// Add escape key listener to panel
panel.addEventListener('keydown', handleAccordionEscapeKey);
} else {
// Remove escape key listener
panel.removeEventListener('keydown', handleAccordionEscapeKey);
}
}
function handleAccordionEscapeKey(event) {
if (event.key === 'Escape') {
const panel = event.target.closest('[hidden]');
if (panel) {
const button = document.querySelector(`[aria-controls="${panel.id}"]`);
if (button) {
button.setAttribute('aria-expanded', 'false');
panel.hidden = true;
button.focus(); // Return focus to header
panel.removeEventListener('keydown', handleAccordionEscapeKey);
}
}
}
}
```
**Using Native Heading:**
```html
<div class="accordion-item">
<h3 id="header-2">
<button
aria-expanded="false"
aria-controls="panel-2"
>
Section Title
</button>
</h3>
<div
id="panel-2"
aria-labelledby="header-2"
>
<p>Panel content...</p>
</div>
</div>
```
**JavaScript Considerations:**
- Implement Enter and Space key handlers for expansion/collapse
- Optionally implement Arrow key navigation between headers
- Update aria-expanded state when panels toggle
- Consider implementing single-expand vs multi-expand behavior
- Use hidden attribute or CSS to show/hide panels (note: CSS visibility property can be animated)
- Ensure smooth keyboard navigation flow
- Implement Escape key handler to close open panel and return focus to header
- Add/remove event listeners when panels open/close to manage Escape key support
**Accessibility Notes:**
- Role 'region' helps screen readers understand panel structure
- Avoid role='region' proliferation with many simultaneous panels
- Button should be direct child of heading element
- Consider aria-disabled='true' for panels that cannot be collapsed
- Test with screen readers to ensure proper announcement
metadata:
priority: high
version: 1.0
</rule>
================================================
FILE: .cursor/rules/animation-accessibility.mdc
================================================
---
description: Enforce animation accessibility standards per WCAG 2.2.2 Pause Stop Hide, 2.3.1 Three Flashes or Below Threshold, and 2.3.3 Animation from Interactions requirements
globs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.css, *.scss, *.sass, *.less
alwaysApply: false
---
# Animation Accessibility Standards
Ensures animations follow WCAG compliance and provide inclusive motion design for users with different accessibility needs including photosensitivity, motion sickness, and cognitive impairments.
<rule>
name: animation_accessibility_standards
description: Enforce animation accessibility standards per WCAG 2.2.2 Pause Stop Hide, 2.3.1 Three Flashes or Below Threshold, and 2.3.3 Animation from Interactions requirements
filters:
- type: file_extension
pattern: "\\.(vue|jsx|tsx|html|liquid|php|js|ts|css|scss|sass|less)$"
actions:
- type: enforce
conditions:
# Missing prefers-reduced-motion media query for animations
- pattern: "(animation|transition|transform|@keyframes)"
pattern_negate: "@media\\s*\\(prefers-reduced-motion:\\s*reduce\\)"
message: "Animations should include prefers-reduced-motion: reduce media query to provide safer alternatives for motion-sensitive users."
# Flashing animations exceeding 3Hz frequency
- pattern: "animation.*(?:pulse|flash|blink|flicker)"
pattern_negate: "animation-duration:\\s*[0-9]*\\.?[0-9]+s|animation-duration:\\s*[0-9]*\\.?[0-9]+ms"
message: "Flashing animations must have duration ensuring frequency is below 3Hz (0.33s) to prevent seizures per WCAG 2.3.1."
# Rapid color transitions that may trigger photosensitivity
- pattern: "transition.*color.*[0-9]*\\.?[0-9]+s|transition.*background.*[0-9]*\\.?[0-9]+s"
pattern_negate: "transition.*color.*[0-9]*\\.?[0-9]+s.*[0-9]*\\.?[0-9]+s|transition.*background.*[0-9]*\\.?[0-9]+s.*[0-9]*\\.?[0-9]+s"
message: "Color transitions should be slow and smooth to avoid triggering photosensitivity. Use longer durations and easing functions."
# Large spatial movements without reduced motion alternatives
- pattern: "transform.*translate\\([^)]*[0-9]{2,}[^)]*\\)|transform.*translate\\([^)]*-[0-9]{2,}[^)]*\\)"
pattern_negate: "@media\\s*\\(prefers-reduced-motion:\\s*reduce\\)"
message: "Large spatial movements should have reduced motion alternatives using prefers-reduced-motion media query."
# Parallax effects without user control
- pattern: "(parallax|scroll.*jack|scroll.*hijack)"
pattern_negate: "(prefers-reduced-motion|user.*control|pause.*animation)"
message: "Parallax and scroll jacking effects should respect user motion preferences and provide controls to pause/disable."
# Auto-playing animations without pause controls
- pattern: "animation.*infinite|animation.*loop"
pattern_negate: "(pause.*control|user.*control|prefers-reduced-motion)"
message: "Looping animations must provide pause controls and respect prefers-reduced-motion preferences."
# Unexpected system-triggered animations
- pattern: "animation.*(?:appear|fade.*in|slide.*in)"
pattern_negate: "(user.*interaction|click|hover|focus|prefers-reduced-motion)"
message: "System-triggered animations should be subtle and respect user motion preferences."
# Missing animation alternatives for essential UI changes
- pattern: "(?:loading|spinner|progress|status)"
pattern_negate: "(animation|transition|@keyframes)"
message: "Essential UI elements like loading indicators should have appropriate animations to communicate state changes."
# Excessive animation duration that may cause motion sickness
- pattern: "animation-duration:\\s*[5-9]\\.[0-9]+s|animation-duration:\\s*[0-9]{2,}s"
message: "Long animation durations may cause motion sickness. Consider shorter durations and provide reduced motion alternatives."
# Missing focus indicators for animated interactive elements
- pattern: "(?:button|a|input|select|textarea).*\\{[^}]*animation"
pattern_negate: "(focus|focus-visible|outline|box-shadow)"
message: "Animated interactive elements must have visible focus indicators for keyboard navigation accessibility."
# Animation without meaningful purpose or context
- pattern: "animation.*(?:bounce|wiggle|shake|rotate)"
pattern_negate: "(loading|status|feedback|interaction)"
message: "Animations should serve a meaningful purpose. Avoid decorative animations that may distract or confuse users."
# Missing animation state management
- pattern: "animation.*(?:play|pause|stop)"
pattern_negate: "(prefers-reduced-motion|user.*control|aria.*live)"
message: "Animation state changes should be communicated to assistive technology and respect user preferences."
- type: suggest
message: |
**Animation Accessibility Best Practices:**
**1. Respect Motion Preferences (WCAG 2.3.3):**
```css
/* Default animation */
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
/* Reduced motion alternative */
@media (prefers-reduced-motion: reduce) {
.fade-in {
animation: none;
opacity: 1;
}
}
```
**2. Seizure Prevention (WCAG 2.3.1):**
```css
/* Safe flashing animation - below 3Hz threshold */
.pulse {
animation: pulse 0.4s ease-in-out infinite;
}
/* Reduced motion alternative */
@media (prefers-reduced-motion: reduce) {
.pulse {
animation: none;
opacity: 0.8;
}
}
```
**3. Motion Sickness Prevention:**
```css
/* Gentle, predictable animations */
.slide-in {
transform: translateX(20px);
transition: transform 0.2s ease-out;
}
/* Reduced motion alternative */
@media (prefers-reduced-motion: reduce) {
.slide-in {
transform: none;
transition: none;
opacity: 1;
}
}
```
**4. Essential UI Animations:**
```css
/* Loading spinner - essential for user understanding */
.loading-spinner {
animation: spin 1s linear infinite;
}
/* Keep essential animations even with reduced motion */
@media (prefers-reduced-motion: reduce) {
.loading-spinner {
animation: spin 2s linear infinite; /* Slower but still visible */
}
}
```
**5. User Control Implementation:**
```javascript
// Animation pause control
class AnimationController {
constructor() {
this.isPaused = false;
this.animations = document.querySelectorAll('[data-animation]');
this.setupControls();
}
setupControls() {
const pauseButton = document.getElementById('pause-animations');
if (pauseButton) {
pauseButton.addEventListener('click', () => this.togglePause());
}
}
togglePause() {
this.isPaused = !this.isPaused;
this.animations.forEach(animation => {
if (this.isPaused) {
animation.style.animationPlayState = 'paused';
} else {
animation.style.animationPlayState = 'running';
}
});
}
}
```
**6. CSS Animation Best Practices:**
```css
/* Safe animation defaults */
.safe-animation {
/* Keep travel distances short */
transform: translateY(10px);
/* Use gentle easing */
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
transform: none;
transition: none;
}
}
/* Fade as safe default */
.fade-transition {
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.fade-transition.visible {
opacity: 1;
}
/* Reduced motion alternative */
@media (prefers-reduced-motion: reduce) {
.fade-transition {
transition: none;
opacity: 1;
}
}
```
**7. JavaScript Animation Control:**
```javascript
// Check motion preferences
function shouldReduceMotion() {
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Safe animation function
function safeAnimate(element, animation, options = {}) {
if (shouldReduceMotion()) {
// Provide alternative experience
element.style.opacity = '1';
element.style.transform = 'none';
return;
}
// Apply animation
element.style.animation = animation;
// Add pause control
if (options.looping) {
element.addEventListener('click', () => {
element.style.animationPlayState =
element.style.animationPlayState === 'paused' ? 'running' : 'paused';
});
}
}
```
**8. HTML Structure for Animation Control:**
```html
<!-- Animation control panel -->
<div class="animation-controls" role="region" aria-label="Animation controls">
<button id="pause-animations" aria-pressed="false">
Pause All Animations
</button>
<button id="reduce-motion" aria-pressed="false">
Reduce Motion
</button>
</div>
<!-- Animated element with controls -->
<div class="animated-element"
data-animation="fade-in"
aria-live="polite">
Content that animates
</div>
```
**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
</rule>
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 %}
<div
{{ block.shopify_attributes }}
class='block-name'
>
<!-- Block content using block.settings -->
</div>
{% 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
<!-- templates/product.liquid -->
<div class='product-page'>
{% comment %} Static breadcrumb block {% endcomment %}
{% content_for 'block', type: 'breadcrumb', id: 'product-breadcrumb' %}
<div class='product-main'>
<div class='product-media'>
{% comment %} Static product gallery block {% endcomment %}
{%
content_for 'block', type: 'product-gallery', id: 'main-gallery', settings: {
enable_zoom: true,
thumbnails_position: "bottom"
}
%}
</div>
<div class='product-info'>
{% comment %} Static product info blocks {% endcomment %}
{% content_for 'block', type: 'product-title', id: 'product-title' %}
{% content_for 'block', type: 'product-price', id: 'product-price' %}
{% content_for 'block', type: 'product-form', id: 'product-form' %}
{% comment %} Dynamic blocks area for additional content {% endcomment %}
<div class='product-extra-content'>
{% content_for 'blocks' %}
</div>
</div>
</div>
{% comment %} Static related products block {% endcomment %}
{%
content_for 'block', type: 'related-products', id: 'related-products', settings: {
heading: "You might also like",
limit: 4
}
%}
</div>
```
**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 %}
<div class='layout-a'>
{{ blocks_content }}
</div>
{% else %}
<div class='layout-b'>
{{ blocks_content }}
</div>
{% endif %}
```
```liquid
{% comment %} ❌ INCORRECT - Multiple content_for calls will cause errors {% endcomment %}
{% if condition %}
<div class='layout-a'>
{% content_for 'blocks' %}
<!-- First call -->
</div>
{% else %}
<div class='layout-b'>
{% content_for 'blocks' %}
<!-- ERROR: Duplicate entry -->
</div>
{% endif %}
```
**Common Error Message:**
```
Liquid syntax error: Duplicate entries for 'content_for "blocks"'
```
### Rendering Nested Blocks
```liquid
<div
class='block-container'
{{ block.shopify_attributes }}
>
<h2>{{ block.settings.heading | escape }}</h2>
<div class='nested-blocks'>
{% content_for 'blocks' %}
</div>
</div>
```
### Nesting with Layout Control
```liquid
<div
class='group {{ block.settings.layout_direction }}'
style='--gap: {{ block.settings.gap }}px;'
{{ block.shopify_attributes }}
>
{% content_for 'blocks' %}
</div>
```
### Presets with Nested Blocks
```json
{
"presets": [
{
"name": "t:names.two_column_layout",
"category": "Layout",
"settings": {
"layout_direction": "horizontal"
},
"blocks": [
{
"type": "text",
"settings": {
"text": "Column 1 content"
}
},
{
"type": "text",
"settings": {
"text": "Column 2 content"
}
}
]
}
]
}
```
When blocks are declared as an object instead of an array, include `block_order`:
```javascript
blocks: {
header: {
type: 'group',
blocks: {
title: { type: 'product-title' },
price: { type: 'price' },
},
block_order: ['title', 'price'],
},
}
```
## CSS and Styling
See [css-standards.mdc](mdc:.cursor/rules/css-standards.mdc) for rules on writing CSS
### Scoped Styles
```liquid
{% stylesheet %}
.block-name {
padding: var(--block-padding, 1rem);
background: var(--block-background, transparent);
}
.block-name__title {
font-size: var(--title-size, 1.5rem);
color: var(--title-color, inherit);
}
.block-name--primary {
background-color: var(--color-primary);
}
.block-name--secondary {
background-color: var(--color-secondary);
}
{% endstylesheet %}
```
### Dynamic CSS Variables
```liquid
<div
class="custom-block"
style="
--block-padding: {{ block.settings.padding }}px;
--text-align: {{ block.settings.alignment }};
--background: {{ block.settings.background_color }};
"
{{ block.shopify_attributes }}
>
```
## Block Targeting
### Section Schema for Theme Blocks
```json
{
"blocks": [
{ "type": "@theme" }, // Accept all theme blocks
{ "type": "@app" } // Accept app blocks
]
}
```
### Restricted Block Targeting
```json
{
"blocks": [
{
"type": "text",
"name": "Text Content"
},
{
"type": "image",
"name": "Image Content"
}
]
}
```
## Common Block Patterns
### Content Block
```liquid
<div
class='content-block {{ block.settings.style }}'
{{ block.shopify_attributes }}
>
{% if block.settings.heading != blank %}
<h3 class='content-block__heading'>{{ block.settings.heading | escape }}</h3>
{% endif %}
{% if block.settings.text != blank %}
<div class='content-block__text'>{{ block.settings.text }}</div>
{% endif %}
{% if block.settings.button_text != blank %}
<a
href='{{ block.settings.button_url }}'
class='content-block__button'
>
{{ block.settings.button_text | escape }}
</a>
{% endif %}
</div>
```
### Media Block
```liquid
<div
class='media-block'
{{ block.shopify_attributes }}
>
{% if block.settings.image %}
<div class='media-block__image'>
{{
block.settings.image
| image_url: width: 800
| image_tag: alt: block.settings.image.alt
| default: block.settings.alt_text
}}
</div>
{% endif %}
{% if block.settings.video %}
<div class='media-block__video'>
{{ block.settings.video | video_tag: controls: true }}
</div>
{% endif %}
</div>
```
### Layout Block (Container)
```liquid
<div
class='layout-block layout-block--{{ block.settings.layout_type }}'
style='
--columns: {{ block.settings.columns }};
--gap: {{ block.settings.gap }}px;
'
{{ block.shopify_attributes }}
>
{% content_for 'blocks' %}
</div>
```
## 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 %}
<div
class='block-content'
{{ block.shopify_attributes }}
>
<!-- Content here -->
</div>
{% 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.
<rule>
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)<nav[^>]*(?:breadcrumb|navigation)[^>]*>"
pattern_negate: "(aria-label|aria-labelledby)=\"[^\"]+\""
message: "Breadcrumb navigation must have aria-label or aria-labelledby attribute."
# Current page aria-current requirement
- pattern: "(?i)<[^>]*(?:breadcrumb.*current|current.*breadcrumb)[^>]*>"
pattern_negate: "aria-current=\"page\""
message: "Current page in breadcrumb must have aria-current='page' attribute."
# List structure requirement
- pattern: "(?i)<nav[^>]*(?:breadcrumb|navigation)[^>]*>"
pattern_negate: "<ol[^>]*>"
message: "Breadcrumb navigation should use ordered list (ol) for proper structure."
- type: suggest
message: |
**Breadcrumb Component Accessibility Best Practices:**
**Required ARIA Attributes:**
- **aria-label/aria-labelledby:** On navigation element to describe the breadcrumb trail
- **aria-current="page":** On the current page link or element
- **role="navigation":** Implicit on nav element, but can be explicit if needed
**Structure Requirements:**
- Use `<nav>` element as container
- Use ordered list (`<ol>`) for breadcrumb items
- Use list items (`<li>`) for each breadcrumb level
- Current page should be the last item in the list
- Use appropriate heading level for the breadcrumb container
**Implementation Patterns:**
**Basic Breadcrumb:**
```html
<nav aria-label="Breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/">Home</a>
</li>
<li class="breadcrumb-item">
<a href="/products">Products</a>
</li>
<li class="breadcrumb-item" aria-current="page">
<a href="/products/electronics">Electronics</a>
</li>
</ol>
</nav>
```
**With aria-labelledby:**
```html
<nav aria-labelledby="breadcrumb-heading">
<h2 id="breadcrumb-heading" class="visually-hidden">Breadcrumb Navigation</h2>
<ol class="breadcrumb">
<li class="breadcrumb-item">
<a href="/">Home</a>
</li>
<li class="breadcrumb-item" aria-current="page">
<span>Current Page</span>
</li>
</ol>
</nav>
```
**Accessibility Notes:**
- Navigation landmark helps screen readers identify the breadcrumb trail
- Ordered list provides semantic structure for the navigation hierarchy
- aria-current helps users identify their current location
- Consider using visually-hidden text for better screen reader context
- Ensure sufficient color contrast for all breadcrumb elements
- Maintain clear visual separation between items
- Use clear, descriptive labels for each level
**Testing Checklist:**
- Verify navigation landmark is present and properly labeled
- Confirm ordered list structure is used
- Check aria-current is present on current page
- Test with screen readers to ensure proper announcement
- Verify visual hierarchy is clear and consistent
- Ensure all links are keyboard accessible
- Check color contrast meets WCAG requirements
metadata:
priority: high
version: 1.0
</rule>
================================================
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.
<rule>
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)<button[^>]*(?: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)<button[^>]*(?: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)<button[^>]*(?: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
<div role="region"
aria-roledescription="carousel"
aria-label="Featured Products"
onmouseenter="pauseRotation()"
onmouseleave="resumeRotation()">
<div class="carousel-container" aria-live="off">
<button aria-label="Stop slide rotation">
Pause
</button>
<div role="group"
aria-roledescription="slide"
aria-label="Product 1 of 3">
<img src="product1.jpg" alt="Product 1">
<h3>Product 1</h3>
</div>
<button aria-label="Previous slide">
Previous
</button>
<button aria-label="Next slide">
Next
</button>
</div>
</div>
```
**Tab Controls Implementation:**
```html
<div role="region"
aria-roledescription="carousel"
aria-label="Featured Products"
onmouseenter="pauseRotation()"
onmouseleave="resumeRotation()">
<div class="carousel-container" aria-live="off">
<!-- Play/Pause Button -->
<button aria-label="Stop slide rotation"
onclick="toggleRotation()"
onkeydown="handleKeyPress(event)">
Pause
</button>
<!-- Slides -->
<div role="group"
aria-roledescription="slide"
aria-label="Product 1 of 3">
<img src="product1.jpg" alt="Product 1">
<h3>Product 1</h3>
</div>
<!-- Navigation Controls -->
<button aria-label="Previous slide"
onclick="previousSlide()"
onkeydown="handleKeyPress(event)">
Previous
</button>
<button aria-label="Next slide"
onclick="nextSlide()"
onkeydown="handleKeyPress(event)">
Next
</button>
<!-- Tab Controls -->
<div role="tablist" aria-label="Slide navigation" class="carousel-tabs">
<button role="tab"
aria-selected="true"
aria-controls="slide-1"
id="tab-1"
onclick="goToSlide(0)"
onkeydown="handleTabKeyPress(event)">
Slide 1
</button>
<button role="tab"
aria-selected="false"
aria-controls="slide-2"
id="tab-2"
onclick="goToSlide(1)"
onkeydown="handleTabKeyPress(event)">
Slide 2
</button>
<button role="tab"
aria-selected="false"
aria-controls="slide-3"
id="tab-3"
onclick="goToSlide(2)"
onkeydown="handleTabKeyPress(event)">
Slide 3
</button>
</div>
</div>
</div>
<style>
/* Tab Controls Styles with WCAG 2.2 compliant contrast */
.carousel-tabs {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.carousel-tabs [role="tab"] {
padding: 0.5rem 1rem;
border: 2px solid #495057; /* 8.3:1 contrast with white */
background: #ffffff;
color: #212529; /* 16.6:1 contrast with white */
border-radius: 4px;
cursor: pointer;
font-weight: 500;
}
/* Selected tab state */
.carousel-tabs [role="tab"][aria-selected="true"] {
background: #0056b3; /* 7.7:1 contrast with white */
color: #ffffff;
border-color: #004085;
}
/* Unselected tab state - still maintaining 4.5:1 contrast */
.carousel-tabs [role="tab"][aria-selected="false"] {
background: #ffffff;
color: #495057; /* 8.3:1 contrast with white */
border-color: #6c757d; /* 5.4:1 contrast with white */
}
/* Focus state */
.carousel-tabs [role="tab"]:focus {
outline: 3px solid #0056b3; /* 7.7:1 contrast with white */
outline-offset: 2px;
}
/* Hover state */
.carousel-tabs [role="tab"]:hover {
background: #f8f9fa;
border-color: #0056b3;
}
/* Hover state for selected tab */
.carousel-tabs [role="tab"][aria-selected="true"]:hover {
background: #004085;
}
/* High contrast mode support */
@media (prefers-contrast: more) {
.carousel-tabs [role="tab"] {
border: 3px solid #000000;
color: #000000;
}
.carousel-tabs [role="tab"][aria-selected="true"] {
background: #000000;
color: #ffffff;
}
.carousel-tabs [role="tab"]:focus {
outline: 3px solid #000000;
}
}
</style>
<script>
// Tab Controls JavaScript
function handleTabKeyPress(event) {
const tabs = Array.from(document.querySelectorAll('[role="tab"]'));
const currentTab = event.target;
const currentIndex = tabs.indexOf(currentTab);
switch (event.key) {
case 'ArrowLeft':
event.preventDefault();
const prevTab = tabs[currentIndex - 1] || tabs[tabs.length - 1];
prevTab.focus();
break;
case 'ArrowRight':
event.preventDefault();
const nextTab = tabs[currentIndex + 1] || tabs[0];
nextTab.focus();
break;
case 'Home':
event.preventDefault();
tabs[0].focus();
break;
case 'End':
event.preventDefault();
tabs[tabs.length - 1].focus();
break;
}
}
function goToSlide(index) {
// Update tab states
const tabs = document.querySelectorAll('[role="tab"]');
tabs.forEach((tab, i) => {
tab.setAttribute('aria-selected', i === index);
});
// Update slide visibility
const slides = document.querySelectorAll('[role="group"]');
slides.forEach((slide, i) => {
slide.setAttribute('aria-hidden', i !== index);
});
// Pause rotation when manually navigating
pauseRotation();
}
</script>
```
**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
</rule>
================================================
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.
<rule>
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)<button[^>]*(?: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)<button[^>]*(?: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)<button[^>]*(?: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)<button[^>]*(?: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)<input[^>]*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
<button class="cart-activator"
aria-haspopup="dialog"
aria-label="View shopping cart"
onclick="openCartDrawer()">
<svg aria-hidden="true" width="24" height="24">
<!-- Cart icon -->
</svg>
<span class="cart-count">3</span>
</button>
```
**Cart Drawer Container:**
```html
<div role="dialog"
aria-modal="true"
aria-labelledby="cart-title"
class="cart-drawer"
id="cart-drawer">
<button type="button"
aria-label="Close cart"
onclick="closeCartDrawer()"
class="cart-close">×</button>
<h2 id="cart-title">Shopping Cart</h2>
<div class="cart-items">
<!-- Cart items -->
</div>
<div class="cart-summary">
<p>Total: $99.99</p>
<button aria-label="Proceed to checkout"
onclick="proceedToCheckout()">
Checkout
</button>
</div>
</div>
```
**Quantity Controls with aria-live:**
```html
<div class="quantity-controls">
<button class="quantity-btn decrease-btn"
aria-label="Decrease quantity"
onclick="updateQuantity(itemId, -1)">-</button>
<input type="number"
class="quantity-input"
value="1"
min="1"
aria-label="Quantity for Product Name"
aria-live="polite"
onchange="setQuantity(itemId, this.value)">
<button class="quantity-btn increase-btn"
aria-label="Increase quantity"
onclick="updateQuantity(itemId, 1)">+</button>
</div>
```
**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
</rule>
================================================
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.
<rule>
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)<div[^>]*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\"|<button)"
message: "Chat controls styled as buttons must have role='button' or use native button elements for proper semantics."
# Chat window missing expandable state management
- pattern: "(?i)<(button|div)[^>]*(?: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)<img[^>]*(?: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 launcher button -->
<button id="chat-launcher"
aria-haspopup="dialog">
<img src="chat-icon.png" alt="" />
Chat with Support
<span id="notification-badge" class="notification-badge" hidden>0</span>
</button>
**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 window -->
<div id="chat-window"
role="dialog"
aria-modal="true"
aria-labelledby="chat-heading"
hidden>
<div class="chat-header">
<h2 id="chat-heading">Chat: Customer Support</h2>
<button type="button"
aria-label="Close chat"
onclick="closeChat()">
×
</button>
</div>
<!-- Message log -->
<div class="chat-messages"
role="log"
aria-labelledby="chat-heading">
<div class="message agent" role="article">
<div class="message-content">
<span class="sr-only">Customer Support: </span>
Hi, how can I help you today?
</div>
<time class="message-time" datetime="2024-01-15T10:00:00Z">10:00 AM</time>
</div>
<div class="message user" role="article">
<div class="message-content">
<span class="sr-only">You sent: </span>
I'm having trouble resetting my password.
</div>
<time class="message-time" datetime="2024-01-15T10:01:00Z">10:01 AM</time>
</div>
</div>
<!-- Input area -->
<div class="chat-input">
<label for="message-input" class="visually-hidden">Type your message:</label>
<textarea id="message-input"
placeholder="Type your message..."
aria-label="Type your message"></textarea>
<button type="button"
aria-label="Send message"
onclick="sendMessage()">
Send
</button>
</div>
</div>
```
**2. Chat Message Structure:**
```html
<!-- Good: Proper message structure with author identification -->
<div class="chat-messages" role="log" aria-labelledby="chat-heading">
<div class="message agent">
<div class="message-content">
<span class="sr-only">Customer Support: </span>
Hi, how can I help you today?
</div>
<time class="message-time" datetime="2024-01-15T10:00:00Z">10:00 AM</time>
</div>
<div class="message user">
<div class="message-content">
<span class="sr-only">You sent: </span>
I'm having trouble resetting my password.
</div>
<time class="message-time" datetime="2024-01-15T10:01:00Z">10:01 AM</time>
</div>
<div class="message agent">
<div class="message-content">
<span class="sr-only">Customer Support: </span>
I can help with that. What's your email address?
</div>
<time class="message-time" datetime="2024-01-15T10:01:30Z">10:01 AM</time>
</div>
</div>
**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.
<!-- Bad: Separate columns that break reading order -->
<div class="chat-columns">
<div class="left-column">
<p>Customer Support: Hi</p>
<p>Customer Support: How can I help?</p>
</div>
<div class="right-column">
<p>Me: Hi</p>
<p>Me: I need help</p>
</div>
</div>
```
**3. Multi-Sensory Notifications:**
```html
<!-- Status message for new messages -->
<div role="status" aria-live="polite" class="chat-status">
New message received
</div>
<!-- Visual notification -->
<div class="chat-notification" aria-hidden="true">
<span class="notification-badge">1</span>
</div>
<!-- Page title update -->
<script>
function updatePageTitle(message) {
const originalTitle = document.title;
document.title = `New message - ${originalTitle}`;
// Restore title after 5 seconds
setTimeout(() => {
document.title = originalTitle;
}, 5000);
}
</script>
```
**4. Enhanced Notification System:**
```html
<!-- Chat launcher with notification badge -->
<button id="chat-launcher"
class="chat-launcher"
aria-haspopup="dialog">
<img src="chat-icon.png" alt="" />
Chat with Support
<span id="notification-badge" class="notification-badge" hidden>0</span>
</button>
**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
<!-- Dynamic accessible name based on message count -->
<button id="chat-launcher"
class="chat-launcher"
aria-haspopup="dialog">
<img src="chat-icon.png" alt="" />
Chat with Support
<span id="notification-badge" class="notification-badge" hidden>0</span>
</button>
```
**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');
}
```
<!-- Persistent notification area for screen readers -->
<div id="chat-notifications"
role="status"
aria-live="polite"
class="sr-only"
aria-label="Chat notifications">
</div>
<style>
.notification-badge {
position: absolute;
top: -5px;
right: -5px;
background: #dc3545;
color: white;
border-radius: 50%;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: bold;
transition: all 0.3s ease;
}
.notification-badge.show {
animation: pulse 2s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
</style>
<script>
let messageCount = 0;
// Update notification badge
function updateNotificationBadge() {
const badge = document.getElementById('notification-badge');
const chatWindow = document.getElementById('chat-window');
const launcher = document.getElementById('chat-launcher');
if (badge) {
// Only show badge when chat is closed
if (chatWindow.classList.contains('hidden')) {
badge.hidden = false;
badge.textContent = messageCount;
badge.classList.add('show');
// Update button's accessible name to include message count
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`);
}
// Remove animation after 2 seconds
setTimeout(() => {
badge.classList.remove('show');
}, 2000);
} else {
// Hide badge when chat is open
badge.hidden = true;
}
}
}
// Announce persistent notification for screen readers
function announcePersistentNotification(sender) {
const notificationArea = document.getElementById('chat-notifications');
const chatWindow = document.getElementById('chat-window');
if (notificationArea && chatWindow.classList.contains('hidden')) {
const message = `New message from ${sender === 'agent' ? 'Customer Support' : 'you'}. Chat window is closed.`;
notificationArea.textContent = message;
// Clear notification after 3 seconds
setTimeout(() => {
notificationArea.textContent = '';
}, 3000);
}
}
// Clear notification badge
function clearNotificationBadge() {
const badge = document.getElementById('notification-badge');
const launcher = document.getElementById('chat-launcher');
if (badge) {
badge.hidden = true;
badge.classList.remove('show');
}
// Reset button's accessible name
if (launcher) {
launcher.removeAttribute('aria-label');
}
}
</script>
```
**6. Focus Management:**
```html
<!-- Chat popup with focus management -->
<div role="dialog"
aria-modal="true"
aria-labelledby="chat-heading"
class="chat-popup">
<div class="chat-header">
<h2 id="chat-heading">Chat Support</h2>
<button type="button"
aria-label="Close chat"
onclick="closeChat()">
×
</button>
</div>
<div class="chat-content" tabindex="-1">
<!-- Chat messages and input -->
</div>
</div>
<script>
function openChat() {
const chatWindow = document.getElementById('chat-window');
const launcher = document.getElementById('chat-launcher');
chatWindow.hidden = false;
launcher.setAttribute('aria-expanded', 'true');
// Focus first focusable element in chat
const firstFocusable = chatWindow.querySelector('button, input, textarea');
if (firstFocusable) {
firstFocusable.focus();
}
}
function closeChat() {
const chatWindow = document.getElementById('chat-window');
const launcher = document.getElementById('chat-launcher');
chatWindow.hidden = true;
launcher.setAttribute('aria-expanded', 'false');
// Return focus to launcher
launcher.focus();
}
</script>
```
**7. Session Timeout Management:**
```html
<!-- Timeout warning -->
<div class="timeout-warning" hidden>
<p>This chat will end in <span id="timeout-countdown">20</span> seconds if you do not reply.</p>
<button type="button" onclick="extendSession()">Extend Session</button>
</div>
<!-- Timeout alert for screen readers -->
<div role="alert" aria-live="assertive" class="timeout-alert" hidden>
This chat will end in 20 seconds if you do not reply.
</div>
<script>
let sessionTimeout;
let warningTimeout;
function startSessionTimer() {
// Start 5-minute session timer
sessionTimeout = setTimeout(() => {
showTimeoutWarning();
}, 280000); // 4 minutes 40 seconds
}
function showTimeoutWarning() {
const warning = document.querySelector('.timeout-warning');
const countdown = document.getElementById('timeout-countdown');
const timeoutAlert = document.querySelector('.timeout-alert');
warning.hidden = false;
timeoutAlert.hidden = false;
// 20-second countdown
let timeLeft = 20;
const countdownInterval = setInterval(() => {
timeLeft--;
countdown.textContent = timeLeft;
if (timeLeft <= 0) {
clearInterval(countdownInterval);
endSession();
}
}, 1000);
}
function extendSession() {
// Set session as extended FIRST to prevent any race conditions
isSessionExtended = true;
const warning = document.querySelector('.timeout-warning');
const timeoutAlert = document.querySelector('.timeout-alert');
// Clear any existing warning timeout
if (warningTimeout) {
clearInterval(warningTimeout);
warningTimeout = null;
}
// Clear the main session timeout as well
if (sessionTimeout) {
clearTimeout(sessionTimeout);
sessionTimeout = null;
}
// Hide warning elements
if (warning) warning.hidden = true;
if (timeoutAlert) timeoutAlert.hidden = true;
// Start fresh session timer
startSessionTimer();
}
// Extend session on user interaction
function extendSessionOnInteraction() {
// Only extend if warning is currently showing
const warning = document.querySelector('.timeout-warning');
if (warning && !warning.hidden) {
extendSession();
} else {
// Just reset the session timer
clearTimeout(sessionTimeout);
clearInterval(warningTimeout); // Also clear any warning timeout that might be running
isSessionExtended = true; // Mark session as extended
startSessionTimer();
}
}
// Monitor input for activity
document.getElementById('message-input').addEventListener('input', () => {
// Reset timer on any input
clearTimeout(sessionTimeout);
startSessionTimer();
});
// 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);
});
}
</script>
```
**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
<!-- Message with proper structure -->
<div class="message agent" role="article">
<div class="message-content">
<span class="sr-only">Customer Support: </span>
Hi, how can I help you today?
</div>
<time datetime="2024-01-15T10:30:00Z" class="timestamp">
<span class="sr-only">Message sent at </span>
10:30 AM
</time>
</div>
<!-- Typing indicator -->
<div class="typing-indicator" aria-live="polite">
<span class="visually-hidden">Customer Support is typing</span>
<span class="typing-dots">...</span>
</div>
```
**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
</rule>
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.
<rule>
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
<!-- Good: High contrast icon -->
<svg width="24" height="24" viewBox="0 0 24 24"
aria-label="Close dialog" role="img">
<path fill="#212529"
d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/>
</svg>
<!-- Good: Icon with sufficient background contrast -->
<button class="icon-button" aria-label="Save document">
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="#ffffff"
d="M17 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V7l-4-4z"/>
</svg>
</button>
/* 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
</rule>
================================================
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.
<rule>
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)<input[^>]*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)<input[^>]*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
<fieldset class="color-swatches">
<legend>Color</legend>
<div class="swatch-group" role="radiogroup" aria-labelledby="color-legend">
<input type="radio"
id="color-red"
name="color"
value="red"
class="visually-hidden">
<label for="color-red"
class="color-swatch"
data-label="Red - Classic crimson shade">
<span class="swatch-color" style="background-color: red;"></span>
</label>
<input type="radio"
id="color-blue"
name="color"
value="blue"
class="visually-hidden">
<label for="color-blue"
class="color-swatch"
data-label="Blue - Navy blue shade">
<span class="swatch-color" style="background-color: blue;"></span>
</label>
</div>
</fieldset>
```
**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.
<rule>
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
<div class="combobox-container">
<label for="combobox-input">Select an option:</label>
<input type="text"
id="combobox-input"
role="combobox"
aria-expanded="false"
aria-haspopup="listbox"
aria-controls="listbox-popup"
aria-autocomplete="list">
<ul id="listbox-popup"
role="listbox"
hidden>
<li role="option"
id="option-1"
aria-selected="false">
Option 1
</li>
<li role="option"
id="option-2"
aria-selected="false">
Option 2
</li>
</ul>
<div id="listbox-status"
role="status"
class="visually-hidden">
<!-- Status messages will be dynamically updated -->
</div>
</div>
<style>
.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;
}
#listbox-popup {
list-style: none;
padding: 0;
margin: 0;
}
</style>
<script>
const input = document.getElementById('combobox-input');
const listbox = document.getElementById('listbox-popup');
const statusElement = document.getElementById('listbox-status');
// Status message handling
function updateStatusMessage(count) {
if (count === 0) {
statusElement.textContent = 'No items available';
} else {
statusElement.textContent = `${count} ${count === 1 ? 'item' : 'items'} available`;
}
}
// Show listbox
function showListbox() {
listbox.hidden = false;
input.setAttribute('aria-expanded', 'true');
const options = listbox.querySelectorAll('[role="option"]');
updateStatusMessage(options.length);
}
// Hide listbox
function hideListbox() {
listbox.hidden = true;
input.setAttribute('aria-expanded', 'false');
input.removeAttribute('aria-activedescendant'); // Important: remove when hiding
statusElement.textContent = '';
}
// Set active option
function setActiveOption(optionId) {
input.setAttribute('aria-activedescendant', optionId);
}
// Example usage:
// When opening listbox with options:
showListbox();
// When setting active option:
setActiveOption('option-1');
// When closing listbox:
hideListbox();
</script>
```
**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
</rule>
================================================
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
<section
style="
--background-color: {{ settings.background_color }};
--padding: {{ settings.padding }}px;
"
>
...
</section>
<button style="--button-color: {{ settings.button_color }};">...</button>
```
❌ Don't do this:
```html
{% style %} .selector--{{ block.id }} { --button-color: {{ settings.button_color }}; } {% endstyle %}
<button class="selector--{{ block.id }}">...</button>
```
### 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
<div class="my-component">
<div class="my-component__wrapper"></div>
</div>
```
❌ Not this:
`.my-component__wrapper` is used as a parent to `.my-component`.
```html
<div class="my-component__wrapper my-component--page-width">
<div class="my-component"></div>
</div>
```
### 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
<div class="my-component my-component--full-width">
<div class="my-component__wrapper">
<button class="my-component__button">
<span class="my-component__button-label">My button</span>
</button>
</div>
</div>
```
✅ Or this:
Started new scope with `.button-component`.
```html
<div class="my-component my-component--full-width">
<div class="my-component__wrapper">
<button class="button-component">
<span class="button-component__label">My button</span>
</button>
</div>
</div>
```
❌ Not this:
Multiple element names are used (`__wrapper__button__label`).
```html
<div class="my-component my-component--full-width">
<div class="my-component__wrapper">
<button class="my-component__wrapper__button">
<span class="my-component__wrapper__button__label">My button</span>
</button>
</div>
</div>
```
### 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
<button class="button button--secondary"></button>
```
❌ Not this:
The `.button` and `.button-secondary` classes are both named as _exclusive_ components and should not used together.
```html
<button class="button button-secondary"></button>
```
❌ Or this:
Modifer class is used without corresponding base classname.
```html
<button class="button--secondary"></button>
```
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
<div class="my-component my-component--size-large my-component--page-width">
<div class="my-component__wrapper"></div>
</div>
```
### 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
<div class="my-component align-left">
<p class="my-component__text"></p>
</div>
```
## 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
* <div class="component component--modifier">
* <div class="component__element">Content</div>
* </div>
*/
.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.
<rule>
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
<!-- ✅ Correct: Content is a sibling to the control -->
<div class="disclosure">
<button type="button"
role="button"
aria-expanded="false"
aria-controls="disclosure-content">
Disclosure Title
</button>
<div id="disclosure-content"
hidden>
Disclosure content goes here...
</div>
</div>
<!-- ❌ Incorrect: Content is not a sibling to the control -->
<div class="disclosure">
<button type="button"
role="button"
aria-expanded="false"
aria-controls="disclosure-content">
Disclosure Title
</button>
</div>
<div class="some-other-container">
<p>Other content...</p>
<div id="disclosure-content" hidden>
Disclosure content goes here...
</div>
</div>
<script>
const button = document.querySelector('[role="button"]');
const content = document.getElementById('disclosure-content');
function toggleDisclosure() {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
content.hidden = isExpanded;
}
// Click handler
button.addEventListener('click', toggleDisclosure);
// Keyboard handler
button.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
toggleDisclosure();
}
});
</script>
```
**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
</rule>
================================================
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.
<rule>
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)<nav[^>]*(?: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)<nav[^>]*(?:navigation|menu|dropdown)[^>]*>"
pattern_negate: "<ul[^>]*>"
message: "Navigation should use unordered list (ul) for proper semantic structure."
# Dropdown button role requirement
- pattern: "(?i)<(button|div|span)[^>]*(?:dropdown|expand|collapse)[^>]*>"
pattern_negate: "role=\"button\""
message: "Dropdown controls must have role='button' (or use native button element which has implicit role)."
# Dropdown button aria-expanded requirement
- pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:dropdown|expand|collapse)[^>]*>"
pattern_negate: "aria-expanded=\"(true|false)\""
message: "Dropdown controls must have aria-expanded attribute set to 'true' or 'false'."
# Dropdown content missing proper identification
- pattern: "(?i)<(div|section)[^>]*(?:dropdown.*content|content.*dropdown)[^>]*>"
pattern_negate: "id=\"[^\"]+\""
message: "Dropdown content must have unique ID attributes for aria-controls reference."
# Missing aria-current on navigation items
- pattern: "(?i)<a[^>]*(?:nav|navigation)[^>]*>"
pattern_negate: "aria-current=\"(page|false)\""
message: "Navigation links should have aria-current attribute set to 'page' for active items or 'false' for inactive."
# Mobile modal missing dialog role
- pattern: "(?i)<(div|section)[^>]*(?:mobile.*nav|nav.*mobile|modal.*nav)[^>]*>"
pattern_negate: "role=\"dialog\""
message: "Mobile navigation modal containers must have role='dialog' attribute."
# Mobile modal missing aria-modal
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"
pattern_negate: "aria-modal=\"true\""
message: "Mobile navigation dialog elements must have aria-modal='true' attribute."
# Mobile modal missing proper labeling
- pattern: "(?i)<[^>]*role=\"dialog\"[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"
pattern_negate: "(aria-labelledby|aria-label)"
message: "Mobile navigation dialog elements must have either aria-labelledby or aria-label for accessibility."
# Mobile launcher missing aria-haspopup
- pattern: "(?i)<button[^>]*(?:mobile.*nav|nav.*mobile|hamburger|menu)[^>]*>"
pattern_negate: "aria-haspopup=\"dialog\""
message: "Mobile navigation launcher buttons must include aria-haspopup='dialog' to inform users a dialog will open."
# Mobile close button missing aria-label
- pattern: "(?i)<button[^>]*(?:close|dismiss|×|×)[^>]*(?:mobile.*nav|nav.*mobile)[^>]*>"
pattern_negate: "aria-label=\"[^\"]*[Cc]lose[^\"]*\""
message: "Mobile navigation close buttons should have aria-label='Close navigation' or similar descriptive text."
# Missing keyboard event handlers for dropdown
- pattern: "(?i)<[^>]*role=\"button\"[^>]*(?:dropdown|expand|collapse)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Dropdown controls should handle keyboard events (Enter, Space, and Escape)."
# Missing Escape key support for dropdown content
- pattern: "(?i)<div[^>]*(?:dropdown.*content|content.*dropdown)[^>]*>"
pattern_negate: "(onKeyDown|onkeydown|@keydown|v-on:keydown)"
message: "Dropdown content areas should handle Escape key to close dropdown and return focus to launcher."
# Incorrect menu role usage
- pattern: "(?i)role=\"(menu|menuitem|menubar|menuitemcheckbox|menuitemradio)\""
message: "Navigation components should NOT use menu roles. Use proper navigation semantics with ul/li/a elements."
# Incorrect aria-haspopup usage
- pattern: "(?i)aria-haspopup=\"(true|menu|listbox)\""
pattern_negate: "aria-haspopup=\"dialog\""
message: "Navigation components should NOT use aria-haspopup except for mobile modal launchers with aria-haspopup='dialog'."
- type: suggest
message: |
**Dropdown Navigation Component Accessibility Best Practices:**
**Navigation Semantics:**
- **role='navigation':** Implicit on nav element, provides landmark
- **aria-label/aria-labelledby:** On nav element to describe the navigation
- **aria-current:** Set on active navigation items ('page' for current page, 'false' for inactive)
- **ul + li + a:** Use semantic list structure for navigation items
- **NO menu roles:** Do not use role="menu", role="menuitem", etc.
**Dropdown Disclosure Pattern:**
- **role='button':** Set on dropdown launcher elements (or use native button)
- **aria-expanded:** 'true' if dropdown content is visible, 'false' if hidden
- **aria-controls:** Reference to the ID of the associated dropdown content
- **aria-current:** Set on active dropdown items
**Mobile Modal Pattern:**
- **role='dialog':** Set on mobile navigation modal container
- **aria-modal='true':** Indicates the dialog is modal
- **aria-labelledby/aria-label:** On mobile dialog for accessibility
- **aria-haspopup='dialog':** Set on mobile launcher button
- **aria-label:** Set on mobile close button
**Keyboard Interaction Requirements:**
- **Tab:** Move through natural tab order on page
- **Enter:** Activate links
- **Space/Enter:** Open/close dropdown when launcher is focused
- **Escape:** Close dropdown component (launcher focused or not)
- **NO auto-open:** Do not open dropdown on keyboard focus
- **Mobile:** Focus management for modal open/close
**Mouse Interaction Requirements:**
- **Hover:** Show underline on links
- **Hover:** Open dropdown, close on mouse-away
- **Click:** Activate links (include mouse-specific click handlers)
- **Mobile:** Click launcher opens modal, click close button closes modal
- **Double-click:** Mobile dropdown launchers navigate to associated page
**Mobile Interaction Requirements:**
- **Three-line button:** Opens modal, focus moves to first focusable item
- **X close button:** Closes modal, focus returns to launcher
- **Dropdown launcher:** Reveals content section, single-click toggles, double-click navigates
- **Focus management:** Proper focus trapping and restoration
**Implementation Patterns:**
**Desktop Navigation Structure:**
```html
<header>
<nav aria-label="Main navigation">
<ul>
<li>
<a href="/home" aria-current="page">Home</a>
</li>
<li>
<button type="button"
role="button"
aria-expanded="false"
aria-controls="products-dropdown">
Products
<span class="chevron">▼</span>
</button>
<div id="products-dropdown" hidden>
<ul>
<li><a href="/products/category1">Category 1</a></li>
<li><a href="/products/category2">Category 2</a></li>
</ul>
</div>
</li>
<li>
<a href="/about" aria-current="false">About</a>
</li>
</ul>
</nav>
</header>
```
**Mobile Navigation Structure:**
```html
<header>
<button type="button"
aria-haspopup="dialog"
aria-label="Open navigation menu"
onclick="openMobileNav()">
<span class="hamburger">☰</span>
</button>
</header>
<div role="dialog"
aria-modal="true"
aria-labelledby="mobile-nav-title"
class="mobile-nav-modal"
hidden>
<button type="button"
aria-label="Close navigation menu"
onclick="closeMobileNav()">
×
</button>
<h2 id="mobile-nav-title">Navigation</h2>
<nav aria-label="Mobile navigation">
<ul>
<li>
<a href="/home" aria-current="page">Home</a>
</li>
<li>
<button type="button"
role="button"
aria-expanded="false"
aria-controls="mobile-products-dropdown"
data-href="/products">
Products
<span class="chevron">▼</span>
</button>
<div id="mobile-products-dropdown" hidden>
<ul>
<li><a href="/products/category1">Category 1</a></li>
<li><a href="/products/category2">Category 2</a></li>
</ul>
</div>
</li>
<li>
<a href="/about" aria-current="false">About</a>
</li>
</ul>
</nav>
</div>
```
**JavaScript for Dropdown Toggle:**
```javascript
function toggleDropdown(button) {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
const content = document.getElementById(button.getAttribute('aria-controls'));
button.setAttribute('aria-expanded', !isExpanded);
content.hidden = isExpanded;
// Update chevron icon
const chevron = button.querySelector('.chevron');
if (chevron) {
chevron.textContent = isExpanded ? '▼' : '▲';
}
if (!isExpanded) {
// Add escape key listener to content
content.addEventListener('keydown', handleDropdownEscapeKey);
} else {
// Remove escape key listener
content.removeEventListener('keydown', handleDropdownEscapeKey);
}
}
function handleDropdownEscapeKey(event) {
if (event.key === 'Escape') {
const content = event.target.closest('[hidden]');
if (content) {
const button = document.querySelector(`[aria-controls="${content.id}"]`);
if (button) {
button.setAttribute('aria-expanded', 'false');
content.hidden = true;
button.focus(); // Return focus to launcher
content.removeEventListener('keydown', handleDropdownEscapeKey);
}
}
}
}
```
**JavaScript for Mobile Modal:**
```javascript
function openMobileNav() {
const modal = document.querySelector('.mobile-nav-modal');
const closeButton = modal.querySelector('button[aria-label*="Close"]');
modal.hidden = false;
closeButton.focus(); // Focus first focusable element
}
function closeMobileNav() {
const modal = document.querySelector('.mobile-nav-modal');
const launcher = document.querySelector('[aria-haspopup="dialog"]');
modal.hidden = true;
launcher.focus(); // Return focus to launcher
}
// Handle escape key for mobile modal
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
const modal = document.querySelector('.mobile-nav-modal');
if (!modal.hidden) {
closeMobileNav();
}
}
});
// Mobile dropdown click and double-click handlers
document.addEventListener('DOMContentLoaded', function() {
const mobileDropdownButtons = document.querySelectorAll('.mobile-dropdown-button');
mobileDropdownButtons.forEach(button => {
// Single click toggles dropdown
button.addEventListener('click', (event) => {
event.preventDefault();
toggleDropdown(button);
});
// Double click navigates to page
button.addEventListener('dblclick', (event) => {
event.preventDefault();
const href = button.getAttribute('data-href') || '/default';
window.location.href = href;
});
});
});
```
**CSS for Hover Interactions:**
```css
/* Link hover underline */
nav a:hover {
text-decoration: underline;
}
/* Dropdown hover behavior */
.dropdown-launcher:hover + .dropdown-content,
.dropdown-content:hover {
display: block;
}
.dropdown-content {
display: none;
}
/* Chevron rotation */
.chevron {
transition: transform 0.2s ease;
}
button[aria-expanded="true"] .chevron {
transform: rotate(180deg);
}
```
**JavaScript Considerations:**
- Implement proper event listeners for all keyboard interactions
- Handle mouse hover events for dropdown display
- Manage focus for mobile modal open/close
- Update aria-current states based on current page
- Handle chevron icon rotation
- Implement proper escape key handling
- Ensure no auto-opening on keyboard focus
- Handle mouse-specific click events for navigation
- Implement double-click navigation for mobile dropdown launchers
**Accessibility Notes:**
- Navigation is NOT a menu - use proper navigation semantics
- Avoid menu roles and aria-haspopup (except mobile modal)
- Ensure proper focus management for mobile modal
- Test with screen readers for proper announcement
- Maintain clear visual indicators for active states
- Consider implementing skip links for large navigation
- Ensure sufficient color contrast for all navigation elements
- Test keyboard navigation flow thoroughly
**Testing Requirements:**
- Test keyboard navigation through all navigation items
- Verify dropdown opens/closes with keyboard and mouse
- Test mobile modal focus management
- Verify screen reader announcement of navigation structure
- Test aria-current updates correctly
- Ensure escape key closes dropdowns and modals
- Test hover interactions work as expected
- Verify no auto-opening on keyboard focus
metadata:
priority: high
version: 1.0
</rule>
description:
globs:
alwaysApply: false
---
description:
globs:
alwaysApply: false
---
================================================
FILE: .cursor/rules/examples/block-example-group.liquid
================================================
{% doc %}
Renders a group of blocks with configurable layout direction, gap and
alignment.
All settings apply to only one dimension to reduce configuration complexity.
This component is a wrapper concerned only with rendering its children in
the specified layout direction with appropriate padding and alignment.
@example
{% content_for 'block', type: 'group', id: 'group' %}
{% enddoc %}
<div
class="group {{ block.settings.layout_direction }}"
style="
--padding: {{ block.settings.padding }}px;
--alignment: {{ block.settings.alignment }};
"
{{ block.shopify_attributes }}
>
{% content_for 'blocks' %}
</div>
{% stylesheet %}
.group {
display: flex;
flex-wrap: nowrap;
overflow: hidden;
width: 100%;
}
.group--horizontal {
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0 var(--padding);
}
.group--vertical {
flex-direction: column;
align-items: var(--alignment);
padding: var(--padding) 0;
}
{% endstylesheet %}
{% schema %}
{
"name": "t:general.group",
"blocks": [{ "type": "@theme" }],
"settings": [
{
"type": "select",
"id": "layout_direction",
"label": "t:labels.layout_direction",
"default": "group--vertical",
"options": [
{ "value": "group--horizontal", "label": "t:options.direction.horizontal" },
{ "value": "group--vertical", "label": "t:options.direction.vertical" }
]
},
{
"visible_if": "{{ block.settings.layout_direction == 'group--vertical' }}",
"type": "select",
"id": "alignment",
"label": "t:labels.alignment",
"default": "flex-start",
"options": [
{ "value": "flex-start", "label": "t:options.alignment.left" },
{ "value": "center", "label": "t:options.alignment.center" },
{ "value": "flex-end", "label": "t:options.alignment.right" }
]
},
{
"type": "range",
"id": "padding",
"label": "t:labels.padding",
"default": 0,
"min": 0,
"max": 200,
"step": 2,
"unit": "px"
}
],
"presets": [
{
"name": "t:general.column",
"category": "t:general.layout",
"settings": {
"layout_direction": "group--vertical",
"alignment": "flex-start",
"padding": 0
}
},
{
"name": "t:general.row",
"category": "t:general.layout",
"settings": {
"layout_direction": "group--horizontal",
"padding": 0
}
}
]
}
{% endschema %}
================================================
FILE: .cursor/rules/examples/block-example-text.liquid
================================================
{% doc %}
Renders a text block.
@example
{% content_for 'block', type: 'text', id: 'text' %}
{% enddoc %}
<div
class="text {{ block.settings.text_style }}"
style="--text-align: {{ block.settings.alignment }}"
{{ block.shopify_attributes }}
>
{{ block.settings.text }}
</div>
{% stylesheet %}
.text {
text-align: var(--text-align);
}
.text--title {
font-size: 2rem;
font-weight: 700;
}
.text--subtitle {
font-size: 1.5rem;
}
{% endstylesheet %}
{% schema %}
{
"name": "t:general.text",
"settings": [
{
"type": "text",
"id": "text",
"label": "t:labels.text",
"default": "Text"
},
{
"type": "select",
"id": "text_style",
"label": "t:labels.text_style",
"options": [
{ "value": "text--title", "label": "t:options.text_style.title" },
{ "value": "text--subtitle", "label": "t:options.text_style.subtitle" },
{ "value": "text--normal", "label": "t:options.text_style.normal" }
],
"default": "text--title"
},
{
"type": "text_alignment",
"id": "alignment",
"label": "t:labels.alignment",
"default": "left"
}
],
"presets": [{ "name": "t:general.text" }]
}
{% endschema %}
================================================
FILE: .cursor/rules/examples/section-example.liquid
================================================
<div class="example-section full-width">
{% if section.settings.background_image %}
<div class="example-section__background">
{{ section.settings.background_image | image_url: width: 2000 | image_tag }}
</div>
{% endif %}
<div class="custom-section__content">
{% content_for 'blocks' %}
</div>
</div>
{% stylesheet %}
.example-section {
position: relative;
overflow: hidden;
width: 100%;
}
.example-section__background {
position: absolute;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
}
.example-section__background img {
position: absolute;
width: 100%;
height: auto;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.example-section__content {
display: grid;
grid-template-columns: var(--content-grid);
}
.example-section__content > * {
grid-column: 2;
}
{% endstylesheet %}
{% schema %}
{
"name": "t:general.custom_section",
"blocks": [{ "type": "@theme" }],
"settings": [
{
"type": "image_picker",
"id": "background_image",
"label": "t:labels.background"
}
],
"presets": [
{
"name": "t:general.custom_section"
}
]
}
{% endschema %}
================================================
FILE: .cursor/rules/examples/snippet-example.liquid
================================================
{% doc %}
Product Card Snippet Template
@param product - {Object} Product object (required)
@param show_vendor - {Boolean} Display vendor name (default: false)
@param show_quick_add - {Boolean} Show quick add button (default: false)
@param image_ratio - {String} Image aspect ratio (default: 'adapt')
@param card_class - {String} Additional CSS classes
@example
{% render 'product-card',
product: product,
show_vendor: true,
image_ratio: 'square'
%}
{% enddoc %}
{% liquid
# Parameter validation and defaults
assign product = product | default: empty
assign show_vendor = show_vendor | default: false
assign show_quick_add = show_quick_add | default: false
assign image_ratio = image_ratio | default: 'adapt'
assign card_class = card_class | default: ''
# Early return if required parameters missing
unless product != empty
echo '<!-- Error: product parameter required for product-card snippet -->'
break
endunless
# Build CSS classes
assign card_classes = 'product-card'
if card_class != blank
assign card_classes = card_classes | append: ' ' | append: card_class
endif
if image_ratio != 'adapt'
assign card_classes = card_classes | append: ' product-card--' | append: image_ratio
endif
%}
<div
class="{{ card_classes }}"
data-product-id="{{ product.id }}"
>
<div class="product-card__media">
{% if product.featured_image %}
<a
href="{{ product.url }}"
class="product-card__link"
>
{{
product.featured_image
| image_url: width: 800
| image_tag: alt: product.featured_image.alt
| default: product.title, loading: 'lazy', class: 'product-card__image'
}}
</a>
{% else %}
<div class="product-card__placeholder">
{{ 'product-1' | placeholder_svg_tag: 'product-card__placeholder-svg' }}
</div>
{% endif %}
</div>
<div class="product-card__info">
<h3 class="product-card__title">
<a href="{{ product.url }}">{{ product.title | escape }}</a>
</h3>
{% if show_vendor and product.vendor != blank %}
<p class="product-card__vendor">{{ product.vendor | escape }}</p>
{% endif %}
{% render 'price', product: product, show_compare_at: true %}
{% if show_quick_add and product.available %}
<div class="product-card__actions">
{% render 'product-form', product: product, form_type: 'quick-add', show_quantity: false %}
</div>
{% endif %}
</div>
</div>
================================================
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.
<rule>
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: "<button[^>]*aria-pressed"
message: "Flip cards must contain a button with aria-pressed attribute to control card state."
# aria-pressed attribute requirement
- pattern: "(?i)<button[^>]*(?: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)<button[^>]*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)<button[^>]*(?:flip|card)[^>]*>"
pattern_negate: "type=\"button\""
message: "Flip card buttons should have type='button' to prevent form submission behavior."
# Incomplete card structure
- pattern: "(?i)<div[^>]*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
<!-- ✅ Correct: Proper flip card structure -->
<div class="card">
<div class="card--front">
<h3>Card Title</h3>
<img src="front-image.jpg" alt="Front view of the product">
</div>
<button type="button"
class="flip-button"
aria-pressed="false"
aria-label="More about Card Title">
</button>
<div class="card--back">
<p class="card--tagline">Inspiring content</p>
<img src="back-image-1.jpg" alt="Product detail view 1">
<img src="back-image-2.jpg" alt="Product detail view 2">
<img src="back-image-3.jpg" alt="Product detail view 3">
<p>More detailed content about the product</p>
<a href="/product-details">
<img src="link-icon.svg" alt="View full product details">
</a>
</div>
</div>
```
**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
</rule>
==================
gitextract_bgoglxxq/
├── .cursor/
│ └── rules/
│ ├── accordion-accessibility.mdc
│ ├── animation-accessibility.mdc
│ ├── assets.mdc
│ ├── blocks.mdc
│ ├── breadcrumb-accessibility.mdc
│ ├── carousel-accessibility.mdc
│ ├── cart-drawer-accessibility.mdc
│ ├── chat-window-accessibility.mdc
│ ├── color-contrast-accessibility.mdc
│ ├── color-swatch-accessibility.mdc
│ ├── combobox-accessibility.mdc
│ ├── css-standards.mdc
│ ├── disclosure-accessibility.mdc
│ ├── dropdown-navigation-accessibility.mdc
│ ├── examples/
│ │ ├── block-example-group.liquid
│ │ ├── block-example-text.liquid
│ │ ├── section-example.liquid
│ │ └── snippet-example.liquid
│ ├── flip-card-accessibility.mdc
│ ├── focus-order-and-styles-accessibility.mdc
│ ├── form-accessibility.mdc
│ ├── global-accessibility-standards.mdc
│ ├── heading-accessibility.mdc
│ ├── html-standards.mdc
│ ├── image-alt-text-accessibility.mdc
│ ├── javascript-standards.mdc
│ ├── landmark-accessibility.mdc
│ ├── liquid.mdc
│ ├── locales.mdc
│ ├── localization.mdc
│ ├── mobile-accessibility-standards.mdc
│ ├── modal-accessibility.mdc
│ ├── product-card-accessibility.mdc
│ ├── product-filter-accessibility.mdc
│ ├── product-media-gallery-accessibility.mdc
│ ├── prompts-and-references.mdc
│ ├── sale-price-accessibility.mdc
│ ├── schemas.mdc
│ ├── sections.mdc
│ ├── slider-accessibility.mdc
│ ├── snippets.mdc
│ ├── switch-accessibility.mdc
│ ├── tab-accessibility.mdc
│ ├── table-accessibility.mdc
│ ├── templates.mdc
│ ├── theme-settings.mdc
│ └── tooltip-accessibility.mdc
├── LICENSE.md
├── README.md
├── assets/
│ ├── accordion-custom.js
│ ├── anchored-popover.js
│ ├── announcement-bar.js
│ ├── auto-close-details.js
│ ├── base.css
│ ├── blog-posts-list.js
│ ├── cart-discount.js
│ ├── cart-drawer.js
│ ├── cart-icon.js
│ ├── cart-note.js
│ ├── collection-links.js
│ ├── comparison-slider.js
│ ├── component-cart-items.js
│ ├── component-cart-quantity-selector.js
│ ├── component-quantity-selector.js
│ ├── component.js
│ ├── copy-to-clipboard.js
│ ├── dialog.js
│ ├── drag-zoom-wrapper.js
│ ├── events.js
│ ├── facets.js
│ ├── floating-panel.js
│ ├── fly-to-cart.js
│ ├── focus.js
│ ├── gift-card-recipient-form.js
│ ├── global.d.ts
│ ├── header-drawer.js
│ ├── header-menu.js
│ ├── header.js
│ ├── jsconfig.json
│ ├── jumbo-text.js
│ ├── layered-slideshow.js
│ ├── local-pickup.js
│ ├── localization.js
│ ├── marquee.js
│ ├── media-gallery.js
│ ├── media.js
│ ├── money-formatting.js
│ ├── morph.js
│ ├── overflow-list.css
│ ├── overflow-list.js
│ ├── paginated-list-aspect-ratio.js
│ ├── paginated-list.js
│ ├── performance.js
│ ├── popover-polyfill.js
│ ├── predictive-search.js
│ ├── price-per-item.js
│ ├── product-card.js
│ ├── product-custom-property.js
│ ├── product-form.js
│ ├── product-hotspot.js
│ ├── product-inventory.js
│ ├── product-price.js
│ ├── product-recommendations.js
│ ├── product-sku.js
│ ├── product-title-truncation.js
│ ├── qr-code-generator.js
│ ├── qr-code-image.js
│ ├── quick-add.js
│ ├── quick-order-list.js
│ ├── recently-viewed-products.js
│ ├── results-list.js
│ ├── rte-formatter.js
│ ├── scrolling.js
│ ├── search-page-input.js
│ ├── section-hydration.js
│ ├── section-renderer.js
│ ├── show-more.js
│ ├── slideshow.js
│ ├── sticky-add-to-cart.js
│ ├── template-giftcard.css
│ ├── theme-editor.js
│ ├── utilities.js
│ ├── variant-picker.js
│ ├── video-background.js
│ ├── view-transitions.js
│ ├── volume-pricing-info.js
│ ├── volume-pricing.js
│ └── zoom-dialog.js
├── blocks/
│ ├── _accordion-row.liquid
│ ├── _announcement.liquid
│ ├── _blog-post-card.liquid
│ ├── _blog-post-content.liquid
│ ├── _blog-post-description.liquid
│ ├── _blog-post-featured-image.liquid
│ ├── _blog-post-image.liquid
│ ├── _blog-post-info-text.liquid
│ ├── _card.liquid
│ ├── _carousel-content.liquid
│ ├── _cart-products.liquid
│ ├── _cart-summary.liquid
│ ├── _cart-title.liquid
│ ├── _collection-card-image.liquid
│ ├── _collection-card.liquid
│ ├── _collection-image.liquid
│ ├── _collection-info.liquid
│ ├── _collection-link.liquid
│ ├── _content-without-appearance.liquid
│ ├── _content.liquid
│ ├── _divider.liquid
│ ├── _featured-blog-posts-card.liquid
│ ├── _featured-blog-posts-image.liquid
│ ├── _featured-blog-posts-title.liquid
│ ├── _featured-product-gallery.liquid
│ ├── _featured-product-information-carousel.liquid
│ ├── _featured-product-price.liquid
│ ├── _featured-product.liquid
│ ├── _footer-social-icons.liquid
│ ├── _header-logo.liquid
│ ├── _header-menu.liquid
│ ├── _heading.liquid
│ ├── _hotspot-product.liquid
│ ├── _image.liquid
│ ├── _inline-collection-title.liquid
│ ├── _inline-text.liquid
│ ├── _layered-slide.liquid
│ ├── _marquee.liquid
│ ├── _media-without-appearance.liquid
│ ├── _media.liquid
│ ├── _product-card-gallery.liquid
│ ├── _product-card-group.liquid
│ ├── _product-card.liquid
│ ├── _product-details.liquid
│ ├── _product-list-button.liquid
│ ├── _product-list-content.liquid
│ ├── _product-list-text.liquid
│ ├── _product-media-gallery.liquid
│ ├── _search-input.liquid
│ ├── _slide.liquid
│ ├── _social-link.liquid
│ ├── accelerated-checkout.liquid
│ ├── accordion.liquid
│ ├── add-to-cart.liquid
│ ├── button.liquid
│ ├── buy-buttons.liquid
│ ├── collection-card.liquid
│ ├── collection-title.liquid
│ ├── comparison-slider.liquid
│ ├── contact-form-submit-button.liquid
│ ├── contact-form.liquid
│ ├── custom-liquid.liquid
│ ├── email-signup.liquid
│ ├── featured-collection.liquid
│ ├── filters.liquid
│ ├── follow-on-shop.liquid
│ ├── footer-copyright.liquid
│ ├── footer-policy-list.liquid
│ ├── group.liquid
│ ├── icon.liquid
│ ├── image.liquid
│ ├── jumbo-text.liquid
│ ├── logo.liquid
│ ├── menu.liquid
│ ├── page-content.liquid
│ ├── page.liquid
│ ├── payment-icons.liquid
│ ├── popup-link.liquid
│ ├── price.liquid
│ ├── product-card.liquid
│ ├── product-custom-property.liquid
│ ├── product-description.liquid
│ ├── product-inventory.liquid
│ ├── product-recommendations.liquid
│ ├── product-title.liquid
│ ├── quantity.liquid
│ ├── review.liquid
│ ├── sku.liquid
│ ├── social-links.liquid
│ ├── spacer.liquid
│ ├── swatches.liquid
│ ├── text.liquid
│ ├── variant-picker.liquid
│ └── video.liquid
├── config/
│ ├── settings_data.json
│ └── settings_schema.json
├── layout/
│ ├── password.liquid
│ └── theme.liquid
├── locales/
│ ├── bg.json
│ ├── cs.json
│ ├── cs.schema.json
│ ├── da.json
│ ├── da.schema.json
│ ├── de.json
│ ├── de.schema.json
│ ├── el.json
│ ├── en.default.json
│ ├── en.default.schema.json
│ ├── es.json
│ ├── es.schema.json
│ ├── fi.json
│ ├── fi.schema.json
│ ├── fr.json
│ ├── fr.schema.json
│ ├── hr.json
│ ├── hu.json
│ ├── id.json
│ ├── it.json
│ ├── it.schema.json
│ ├── ja.json
│ ├── ja.schema.json
│ ├── ko.json
│ ├── ko.schema.json
│ ├── lt.json
│ ├── nb.json
│ ├── nb.schema.json
│ ├── nl.json
│ ├── nl.schema.json
│ ├── pl.json
│ ├── pl.schema.json
│ ├── pt-BR.json
│ ├── pt-BR.schema.json
│ ├── pt-PT.json
│ ├── pt-PT.schema.json
│ ├── ro.json
│ ├── ru.json
│ ├── sk.json
│ ├── sl.json
│ ├── sv.json
│ ├── sv.schema.json
│ ├── th.json
│ ├── th.schema.json
│ ├── tr.json
│ ├── tr.schema.json
│ ├── vi.json
│ ├── zh-CN.json
│ ├── zh-CN.schema.json
│ ├── zh-TW.json
│ └── zh-TW.schema.json
├── release-notes.md
├── sections/
│ ├── _blocks.liquid
│ ├── carousel.liquid
│ ├── collection-links.liquid
│ ├── collection-list.liquid
│ ├── custom-liquid.liquid
│ ├── divider.liquid
│ ├── featured-blog-posts.liquid
│ ├── featured-product-information.liquid
│ ├── featured-product.liquid
│ ├── footer-group.json
│ ├── footer-utilities.liquid
│ ├── footer.liquid
│ ├── header-announcements.liquid
│ ├── header-group.json
│ ├── header.liquid
│ ├── hero.liquid
│ ├── layered-slideshow.liquid
│ ├── logo.liquid
│ ├── main-404.liquid
│ ├── main-blog-post.liquid
│ ├── main-blog.liquid
│ ├── main-cart.liquid
│ ├── main-collection-list.liquid
│ ├── main-collection.liquid
│ ├── main-page.liquid
│ ├── marquee.liquid
│ ├── media-with-content.liquid
│ ├── password-footer.liquid
│ ├── password.liquid
│ ├── predictive-search-empty.liquid
│ ├── predictive-search.liquid
│ ├── product-hotspots.liquid
│ ├── product-information.liquid
│ ├── product-list.liquid
│ ├── product-recommendations.liquid
│ ├── quick-order-list.liquid
│ ├── search-header.liquid
│ ├── search-results.liquid
│ ├── section-rendering-product-card.liquid
│ ├── section.liquid
│ └── slideshow.liquid
├── snippets/
│ ├── add-to-cart-button.liquid
│ ├── background-media.liquid
│ ├── bento-grid.liquid
│ ├── blog-comment-form.liquid
│ ├── border-override.liquid
│ ├── button.liquid
│ ├── card-gallery.liquid
│ ├── cart-bubble.liquid
│ ├── cart-products.liquid
│ ├── cart-summary.liquid
│ ├── checkbox.liquid
│ ├── collection-card.liquid
│ ├── color-schemes.liquid
│ ├── divider.liquid
│ ├── editorial-blog-grid.liquid
│ ├── editorial-collection-grid.liquid
│ ├── editorial-product-grid.liquid
│ ├── filter-remove-buttons.liquid
│ ├── fonts.liquid
│ ├── format-price.liquid
│ ├── gap-style.liquid
│ ├── gift-card-recipient-form.liquid
│ ├── grid-density-controls.liquid
│ ├── group.liquid
│ ├── header-actions.liquid
│ ├── header-drawer.liquid
│ ├── header-row.liquid
│ ├── icon-or-image.liquid
│ ├── icon.liquid
│ ├── image.liquid
│ ├── jumbo-text.liquid
│ ├── layout-panel-style.liquid
│ ├── link-featured-image.liquid
│ ├── list-filter.liquid
│ ├── localization-form.liquid
│ ├── media.liquid
│ ├── mega-menu-list.liquid
│ ├── menu-font-styles.liquid
│ ├── meta-tags.liquid
│ ├── overflow-list.liquid
│ ├── overlay.liquid
│ ├── pagination-controls.liquid
│ ├── password-layout-styles.liquid
│ ├── predictive-search-empty-state.liquid
│ ├── predictive-search-products-list.liquid
│ ├── predictive-search-resource-carousel.liquid
│ ├── price-filter.liquid
│ ├── price.liquid
│ ├── product-card.liquid
│ ├── product-grid.liquid
│ ├── product-information-content.liquid
│ ├── product-media-gallery-content.liquid
│ ├── product-media.liquid
│ ├── quantity-selector.liquid
│ ├── quick-add-modal.liquid
│ ├── quick-add.liquid
│ ├── resource-card.liquid
│ ├── resource-image.liquid
│ ├── resource-list-carousel.liquid
│ ├── resource-list.liquid
│ ├── scripts.liquid
│ ├── search-modal.liquid
│ ├── search.liquid
│ ├── section.liquid
│ ├── size-style.liquid
│ ├── skip-to-content-link.liquid
│ ├── sku.liquid
│ ├── slideshow-arrow.liquid
│ ├── slideshow-arrows.liquid
│ ├── slideshow-controls.liquid
│ ├── slideshow-slide.liquid
│ ├── slideshow.liquid
│ ├── sorting.liquid
│ ├── spacing-padding.liquid
│ ├── spacing-style.liquid
│ ├── strikethrough-variant.liquid
│ ├── stylesheets.liquid
│ ├── submenu-font-styles.liquid
│ ├── swatch.liquid
│ ├── tax-info.liquid
│ ├── text.liquid
│ ├── theme-editor.liquid
│ ├── theme-styles-variables.liquid
│ ├── typography-style.liquid
│ ├── unit-price.liquid
│ ├── util-autofill-img-size-attr.liquid
│ ├── util-mega-menu-img-sizes-attr.liquid
│ ├── util-product-grid-card-size.liquid
│ ├── util-product-media-sizes-attr.liquid
│ ├── variant-main-picker.liquid
│ ├── variant-swatches.liquid
│ ├── video.liquid
│ └── volume-pricing-info.liquid
└── templates/
├── 404.json
├── article.json
├── blog.json
├── cart.json
├── collection.json
├── gift_card.liquid
├── index.json
├── list-collections.json
├── page.contact.json
├── page.json
├── password.json
├── product.json
└── search.json
SYMBOL INDEX (833 symbols across 74 files)
FILE: assets/accordion-custom.js
class AccordionCustom (line 5) | class AccordionCustom extends HTMLElement {
method details (line 7) | get details() {
method summary (line 16) | get summary() {
method #disableOnMobile (line 24) | get #disableOnMobile() {
method #disableOnDesktop (line 28) | get #disableOnDesktop() {
method #closeWithEscape (line 32) | get #closeWithEscape() {
method connectedCallback (line 38) | connectedCallback() {
method disconnectedCallback (line 51) | disconnectedCallback() {
method #setDefaultOpenState (line 81) | #setDefaultOpenState() {
method #handleKeyDown (line 94) | #handleKeyDown(event) {
FILE: assets/anchored-popover.js
class AnchoredPopoverComponent (line 29) | class AnchoredPopoverComponent extends Component {
method connectedCallback (line 96) | connectedCallback() {
method disconnectedCallback (line 124) | disconnectedCallback() {
FILE: assets/announcement-bar.js
class AnnouncementBar (line 15) | class AnnouncementBar extends Component {
method connectedCallback (line 24) | connectedCallback() {
method next (line 34) | next() {
method previous (line 38) | previous() {
method play (line 46) | play(interval = this.autoplayInterval) {
method pause (line 61) | pause() {
method paused (line 66) | get paused() {
method paused (line 70) | set paused(paused) {
method suspend (line 77) | suspend() {
method resume (line 85) | resume() {
method autoplay (line 92) | get autoplay() {
method autoplayInterval (line 96) | get autoplayInterval() {
method current (line 105) | get current() {
method current (line 109) | set current(current) {
FILE: assets/blog-posts-list.js
class BlogPostsList (line 6) | class BlogPostsList extends PaginatedList {}
FILE: assets/cart-discount.js
class CartDiscount (line 19) | class CartDiscount extends Component {
method #createAbortController (line 25) | #createAbortController() {
method #handleDiscountError (line 176) | #handleDiscountError(type) {
method #existingDiscounts (line 187) | #existingDiscounts() {
FILE: assets/cart-drawer.js
class CartDrawerComponent (line 13) | class CartDrawerComponent extends DialogComponent {
method connectedCallback (line 20) | connectedCallback() {
method disconnectedCallback (line 32) | disconnectedCallback() {
method open (line 73) | open() {
method close (line 86) | close() {
method #updateStickyState (line 90) | #updateStickyState() {
FILE: assets/cart-icon.js
class CartIcon (line 15) | class CartIcon extends Component {
method currentCartCount (line 19) | get currentCartCount() {
method currentCartCount (line 23) | set currentCartCount(value) {
method connectedCallback (line 27) | connectedCallback() {
method disconnectedCallback (line 35) | disconnectedCallback() {
FILE: assets/cart-note.js
class CartNote (line 8) | class CartNote extends Component {
FILE: assets/collection-links.js
class CollectionLinks (line 18) | class CollectionLinks extends Component {
method connectedCallback (line 24) | connectedCallback() {
method disconnectedCallback (line 33) | disconnectedCallback() {
method links (line 39) | get links() {
method currentIndex (line 43) | get currentIndex() {
method select (line 53) | select(targetIndex, event) {
method #updateSelectedLink (line 64) | #updateSelectedLink(index) {
method #handleKeydown (line 97) | #handleKeydown(event) {
method #revealImage (line 164) | #revealImage(event) {
FILE: assets/comparison-slider.js
class ComparisonSliderComponent (line 16) | class ComparisonSliderComponent extends Component {
method constructor (line 19) | constructor() {
method connectedCallback (line 28) | connectedCallback() {
method sync (line 46) | sync() {
method disconnectedCallback (line 61) | disconnectedCallback() {
method setValue (line 72) | setValue(value) {
method animateSlider (line 83) | animateSlider() {
method setupIntersectionObserver (line 118) | setupIntersectionObserver() {
method handleIntersection (line 135) | handleIntersection(entries) {
FILE: assets/component-cart-items.js
class CartItemsComponent (line 32) | class CartItemsComponent extends Component {
method connectedCallback (line 35) | connectedCallback() {
method disconnectedCallback (line 43) | disconnectedCallback() {
method #onQuantityChange (line 54) | #onQuantityChange(event) {
method onLineItemRemove (line 83) | onLineItemRemove(line) {
method updateQuantity (line 135) | updateQuantity(config) {
method #disableCartItems (line 265) | #disableCartItems() {
method #enableCartItems (line 272) | #enableCartItems() {
method #updateQuantitySelectors (line 281) | #updateQuantitySelectors(updatedCart) {
method #updateCartQuantitySelectorButtonStates (line 305) | #updateCartQuantitySelectorButtonStates() {
method sectionId (line 315) | get sectionId() {
method isDrawer (line 326) | get isDrawer() {
FILE: assets/component-cart-quantity-selector.js
class CartQuantitySelectorComponent (line 10) | class CartQuantitySelectorComponent extends QuantitySelectorComponent {
method getEffectiveMax (line 16) | getEffectiveMax() {
method updateButtonStates (line 25) | updateButtonStates() {
FILE: assets/component-quantity-selector.js
class QuantitySelectorComponent (line 22) | class QuantitySelectorComponent extends Component {
method connectedCallback (line 28) | connectedCallback() {
method disconnectedCallback (line 45) | disconnectedCallback() {
method setCartQuantity (line 53) | setCartQuantity(cartQty) {
method canAddToCart (line 62) | canAddToCart() {
method getValue (line 79) | getValue() {
method setValue (line 87) | setValue(value) {
method updateConstraints (line 97) | updateConstraints(min, max, step) {
method getCurrentValues (line 134) | getCurrentValues() {
method getEffectiveMax (line 152) | getEffectiveMax() {
method updateButtonStates (line 162) | updateButtonStates() {
method updateQuantity (line 181) | updateQuantity(stepMultiplier) {
method increaseQuantity (line 197) | increaseQuantity(event) {
method decreaseQuantity (line 207) | decreaseQuantity(event) {
method selectInputValue (line 217) | selectInputValue(event) {
method setQuantity (line 229) | setQuantity(event) {
method onQuantityChange (line 256) | onQuantityChange() {
method updateCartQuantity (line 267) | updateCartQuantity() {
method quantityInput (line 286) | get quantityInput() {
FILE: assets/component.js
class DeclarativeShadowElement (line 8) | class DeclarativeShadowElement extends HTMLElement {
method connectedCallback (line 9) | connectedCallback() {
class Component (line 39) | class Component extends DeclarativeShadowElement {
method roots (line 59) | get roots() {
method connectedCallback (line 68) | connectedCallback() {
method updatedCallback (line 90) | updatedCallback() {
method disconnectedCallback (line 100) | disconnectedCallback() {
method #updateRefs (line 109) | #updateRefs() {
function getAncestor (line 178) | function getAncestor(node) {
function getClosestComponent (line 193) | function getClosestComponent(node) {
function registerEventListeners (line 212) | function registerEventListeners() {
function parseData (line 306) | function parseData(str) {
function parseValue (line 327) | function parseValue(str) {
class MissingRefError (line 340) | class MissingRefError extends Error {
method constructor (line 345) | constructor(ref, component) {
FILE: assets/copy-to-clipboard.js
class CopyToClipboardComponent (line 8) | class CopyToClipboardComponent extends Component {
method copyToClipboard (line 9) | copyToClipboard() {
FILE: assets/dialog.js
class DialogComponent (line 12) | class DialogComponent extends Component {
method connectedCallback (line 15) | connectedCallback() {
method disconnectedCallback (line 23) | disconnectedCallback() {
method showDialog (line 46) | showDialog() {
method #handleClick (line 121) | #handleClick(event) {
method #handleKeyDown (line 134) | #handleKeyDown(event) {
method minWidth (line 146) | get minWidth() {
method maxWidth (line 155) | get maxWidth() {
class DialogOpenEvent (line 162) | class DialogOpenEvent extends CustomEvent {
method constructor (line 163) | constructor() {
class DialogCloseEvent (line 170) | class DialogCloseEvent extends CustomEvent {
method constructor (line 171) | constructor() {
FILE: assets/drag-zoom-wrapper.js
constant MIN_ZOOM (line 5) | const MIN_ZOOM = 1;
constant MAX_ZOOM (line 6) | const MAX_ZOOM = 5;
constant DEFAULT_ZOOM (line 7) | const DEFAULT_ZOOM = 1.5;
constant DOUBLE_TAP_DELAY (line 8) | const DOUBLE_TAP_DELAY = 300;
constant DOUBLE_TAP_DISTANCE (line 9) | const DOUBLE_TAP_DISTANCE = 50;
constant DRAG_THRESHOLD (line 10) | const DRAG_THRESHOLD = 10;
class DragZoomWrapper (line 18) | class DragZoomWrapper extends Component {
method #image (line 50) | get #image() {
method connectedCallback (line 54) | connectedCallback() {
method #initResizeListener (line 67) | #initResizeListener() {
method #initEventListeners (line 71) | #initEventListeners() {
method disconnectedCallback (line 85) | disconnectedCallback() {
method #startZoomGestureFromTouches (line 161) | #startZoomGestureFromTouches(touch1, touch2) {
method #startDragGestureFromTouch (line 172) | #startDragGestureFromTouch(touch) {
method #storeTapInfo (line 184) | #storeTapInfo(currentTime, touch) {
method #handleDoubleTapFromTouch (line 193) | #handleDoubleTapFromTouch(touch) {
method #processZoomGesture (line 270) | #processZoomGesture(touch1, touch2) {
method #processDragGesture (line 309) | #processDragGesture(touch) {
method #constrainTranslation (line 346) | #constrainTranslation() {
method #cancelAnimationFrame (line 432) | #cancelAnimationFrame() {
method destroy (line 473) | destroy() {
function getDistance (line 485) | function getDistance(point1, point2) {
FILE: assets/events.js
class ThemeEvents (line 8) | class ThemeEvents {
class VariantSelectedEvent (line 35) | class VariantSelectedEvent extends Event {
method constructor (line 41) | constructor(resource) {
class VariantUpdateEvent (line 53) | class VariantUpdateEvent extends Event {
method constructor (line 73) | constructor(resource, sourceId, data) {
class CartAddEvent (line 91) | class CartAddEvent extends Event {
method constructor (line 104) | constructor(resource, sourceId, data) {
class CartUpdateEvent (line 122) | class CartUpdateEvent extends Event {
method constructor (line 135) | constructor(resource, sourceId, data) {
class CartErrorEvent (line 151) | class CartErrorEvent extends Event {
method constructor (line 159) | constructor(sourceId, message, description, errors) {
class QuantitySelectorUpdateEvent (line 176) | class QuantitySelectorUpdateEvent extends Event {
method constructor (line 182) | constructor(quantity, cartLine) {
class DiscountUpdateEvent (line 195) | class DiscountUpdateEvent extends Event {
method constructor (line 201) | constructor(resource, sourceId) {
class MediaStartedPlayingEvent (line 214) | class MediaStartedPlayingEvent extends Event {
method constructor (line 219) | constructor(resource) {
class SlideshowSelectEvent (line 237) | class SlideshowSelectEvent extends Event {
method constructor (line 239) | constructor(data) {
class ZoomMediaSelectedEvent (line 254) | class ZoomMediaSelectedEvent extends Event {
method constructor (line 259) | constructor(index) {
class MegaMenuHoverEvent (line 271) | class MegaMenuHoverEvent extends Event {
method constructor (line 272) | constructor() {
class FilterUpdateEvent (line 278) | class FilterUpdateEvent extends Event {
method constructor (line 280) | constructor(queryParams) {
method shouldShowClearAll (line 287) | shouldShowClearAll() {
FILE: assets/facets.js
constant SEARCH_QUERY (line 10) | const SEARCH_QUERY = 'q';
class FacetsFormComponent (line 21) | class FacetsFormComponent extends Component {
method createURLParameters (line 29) | createURLParameters(formData = new FormData(this.refs.facetsForm)) {
method #getSearchQuery (line 47) | #getSearchQuery() {
method sectionId (line 52) | get sectionId() {
method #updateURLHash (line 61) | #updateURLHash() {
method #updateSection (line 85) | #updateSection() {
method updateFiltersByURL (line 99) | updateFiltersByURL(url) {
class FacetInputsComponent (line 119) | class FacetInputsComponent extends Component {
method sectionId (line 120) | get sectionId() {
method updateFilters (line 129) | updateFilters() {
method handleKeyDown (line 142) | handleKeyDown(event) {
method #updateSelectedFacetSummary (line 191) | #updateSelectedFacetSummary() {
class PriceFacetComponent (line 218) | class PriceFacetComponent extends Component {
method connectedCallback (line 224) | connectedCallback() {
method disconnectedCallback (line 231) | disconnectedCallback() {
method #extractMoneyPlaceholder (line 241) | #extractMoneyPlaceholder(format) {
method updatePriceFilterAndResults (line 260) | updatePriceFilterAndResults() {
method #parseDisplayValue (line 281) | #parseDisplayValue(displayValue, currency) {
method #adjustToValidValues (line 289) | #adjustToValidValues(input) {
method #setMinAndMaxValues (line 310) | #setMinAndMaxValues() {
method #updateSummary (line 322) | #updateSummary() {
class FacetClearComponent (line 341) | class FacetClearComponent extends Component {
method connectedCallback (line 344) | connectedCallback() {
method disconnectedCallback (line 350) | disconnectedCallback() {
method clearFilter (line 359) | clearFilter(event) {
class FacetRemoveComponent (line 426) | class FacetRemoveComponent extends Component {
method connectedCallback (line 427) | connectedCallback() {
method disconnectedCallback (line 432) | disconnectedCallback() {
method removeFilter (line 443) | removeFilter({ form }, event) {
class SortingFilterComponent (line 490) | class SortingFilterComponent extends Component {
method #moveFocus (line 560) | #moveFocus(options, newIndex) {
method #selectOption (line 580) | #selectOption(option) {
method #closeDropdown (line 600) | #closeDropdown() {
method updateFilterAndSorting (line 628) | updateFilterAndSorting(event) {
method updateFacetStatus (line 681) | updateFacetStatus(event) {
class FacetStatusComponent (line 708) | class FacetStatusComponent extends Component {
method updateListSummary (line 713) | updateListSummary(checkedInputElements) {
method #updateSwatchSummary (line 726) | #updateSwatchSummary(checkedInputElements, checkedInputElementsCount) {
method #updateBubbleSummary (line 754) | #updateBubbleSummary(checkedInputElements, checkedInputElementsCount) {
method updatePriceSummary (line 779) | updatePriceSummary(minInput, maxInput) {
method #parseCents (line 802) | #parseCents(value, fallback = '0', currency = '') {
method #formatMoney (line 821) | #formatMoney(moneyValue) {
method clearSummary (line 833) | clearSummary() {
FILE: assets/floating-panel.js
constant OFFSET (line 4) | const OFFSET = 40;
class FloatingPanelComponent (line 9) | class FloatingPanelComponent extends Component {
method connectedCallback (line 44) | connectedCallback() {
method disconnectedCallback (line 54) | disconnectedCallback() {
FILE: assets/fly-to-cart.js
class FlyToCart (line 8) | class FlyToCart extends Component {
method connectedCallback (line 18) | connectedCallback() {
FILE: assets/focus.js
function getFocusableElements (line 10) | function getFocusableElements(container) {
function trapFocus (line 22) | function trapFocus(container) {
function removeTrapFocus (line 78) | function removeTrapFocus() {
function cycleFocus (line 89) | function cycleFocus(items, increment) {
FILE: assets/gift-card-recipient-form.js
class GiftCardRecipientForm (line 26) | class GiftCardRecipientForm extends Component {
method #inputFields (line 56) | get #inputFields() {
method connectedCallback (line 60) | connectedCallback() {
method disconnectedCallback (line 75) | disconnectedCallback() {
method #initializeForm (line 97) | #initializeForm() {
method toggleRecipientForm (line 112) | toggleRecipientForm(mode, _event) {
method #updateFormState (line 131) | #updateFormState() {
method #updateButtonStates (line 178) | #updateButtonStates(mode) {
method #clearRecipientFields (line 203) | #clearRecipientFields() {
method #disableRecipientFields (line 215) | #disableRecipientFields() {
method #enableRecipientFields (line 240) | #enableRecipientFields() {
method #updateCharacterCount (line 271) | #updateCharacterCount() {
method #setDateConstraints (line 289) | #setDateConstraints() {
method #displayCartError (line 314) | #displayCartError(event) {
method #displayErrorMessage (line 332) | #displayErrorMessage(title, body) {
method #clearErrorMessages (line 381) | #clearErrorMessages() {
method #handleCartAdd (line 408) | #handleCartAdd() {
FILE: assets/global.d.ts
type Shopify (line 4) | interface Shopify {
type Theme (line 18) | interface Theme {
type Window (line 38) | interface Window {
type LoadCallback (line 45) | type LoadCallback = (error: Error | undefined) => void;
type ShopifyFeature (line 48) | interface ShopifyFeature {
type ModelViewer (line 55) | interface ModelViewer {
type Navigator (line 70) | interface Navigator {
FILE: assets/header-drawer.js
class HeaderDrawer (line 14) | class HeaderDrawer extends Component {
method connectedCallback (line 17) | connectedCallback() {
method disconnectedCallback (line 24) | disconnectedCallback() {
method isOpen (line 42) | get isOpen() {
method #getDetailsElement (line 51) | #getDetailsElement(event) {
method toggle (line 60) | toggle() {
method open (line 69) | open(target, event) {
method back (line 95) | back(event) {
method close (line 102) | close() {
method #close (line 111) | #close(details) {
method #setupAnimatedElementListeners (line 144) | #setupAnimatedElementListeners() {
method preventInitialAccordionAnimations (line 157) | preventInitialAccordionAnimations(details) {
function reset (line 184) | function reset(element) {
FILE: assets/header-menu.js
class HeaderMenu (line 17) | class HeaderMenu extends Component {
method connectedCallback (line 25) | connectedCallback() {
method disconnectedCallback (line 33) | disconnectedCallback() {
method overflowMenu (line 62) | get overflowMenu() {
method overflowListHovered (line 70) | get overflowListHovered() {
method headerComponent (line 74) | get headerComponent() {
method deactivate (line 168) | deactivate(event) {
method #getOverflowListLinksHeight (line 210) | #getOverflowListLinksHeight() {
method #setFullOpenHeaderHeight (line 240) | #setFullOpenHeaderHeight(submenuHeight) {
method #cleanupMutationObserver (line 264) | #cleanupMutationObserver() {
function findMenuItem (line 279) | function findMenuItem(element) {
function findSubmenu (line 295) | function findSubmenu(element) {
FILE: assets/header.js
class HeaderComponent (line 21) | class HeaderComponent extends Component {
method #updateMenuVisibility (line 117) | #updateMenuVisibility(hideMenu) {
method connectedCallback (line 185) | connectedCallback() {
method disconnectedCallback (line 200) | disconnectedCallback() {
FILE: assets/jumbo-text.js
class JumboText (line 7) | class JumboText extends Component {
method connectedCallback (line 8) | connectedCallback() {
method disconnectedCallback (line 16) | disconnectedCallback() {
method #setIntersectionObserver (line 29) | #setIntersectionObserver() {
FILE: assets/layered-slideshow.js
constant DRAG_THRESHOLD (line 23) | const DRAG_THRESHOLD = 5;
constant MAX_DRAG_WIDTH_RATIO (line 24) | const MAX_DRAG_WIDTH_RATIO = 0.8;
constant DRAG_COMPLETE_THRESHOLD (line 25) | const DRAG_COMPLETE_THRESHOLD = 0.5;
constant INACTIVE_SIZE (line 26) | const INACTIVE_SIZE = 56;
constant INACTIVE_MOBILE_SIZE (line 27) | const INACTIVE_MOBILE_SIZE = 44;
constant FOCUSABLE_SELECTOR (line 28) | const FOCUSABLE_SELECTOR =
class LayeredSlideshowComponent (line 32) | class LayeredSlideshowComponent extends Component {
method #inactiveSize (line 48) | get #inactiveSize() {
method connectedCallback (line 52) | connectedCallback() {
method #setupEventListeners (line 90) | #setupEventListeners() {
method #handleKeydown (line 111) | #handleKeydown(/** @type {KeyboardEvent} */ e) {
method #handleTabClick (line 135) | #handleTabClick(/** @type {MouseEvent} */ e, /** @type {number} */ ind...
method #handleTabFocus (line 140) | #handleTabFocus(/** @type {FocusEvent} */ e, /** @type {number} */ ind...
method #setupPanelFocusManagement (line 150) | #setupPanelFocusManagement(opts) {
method #handlePanelKeydown (line 163) | #handlePanelKeydown(event, index) {
method #focusPanelEdge (line 199) | #focusPanelEdge(index, position = 'start') {
method #getFocusableElements (line 213) | #getFocusableElements(panel) {
method #preventClickDuringDrag (line 219) | #preventClickDuringDrag(/** @type {MouseEvent} */ e) {
method disconnectedCallback (line 247) | disconnectedCallback() {
method select (line 264) | select(index, { instant = false } = {}) {
method #activate (line 272) | #activate(index, instant = false) {
method #updateActiveTab (line 293) | #updateActiveTab() {
method #updateGridSizes (line 319) | #updateGridSizes(containerSize) {
method #startDrag (line 334) | #startDrag(event) {
method #initializeDrag (line 358) | #initializeDrag(event, initialTarget) {
method #handleDrag (line 388) | #handleDrag(event) {
method #endDrag (line 440) | #endDrag(ac) {
method #observeContentHeight (line 462) | #observeContentHeight() {
method #syncHeight (line 493) | #syncHeight() {
method #syncDesktopHeight (line 509) | #syncDesktopHeight(contentHeight, isAuto) {
method #syncMobileHeight (line 539) | #syncMobileHeight(contentHeight, isAuto) {
method #clearHeightStyles (line 564) | #clearHeightStyles() {
method #getMaxContentHeight (line 572) | #getMaxContentHeight() {
FILE: assets/local-pickup.js
class LocalPickup (line 5) | class LocalPickup extends Component {
method connectedCallback (line 9) | connectedCallback() {
method #createAbortController (line 40) | #createAbortController() {
FILE: assets/localization.js
class LocalizationFormComponent (line 21) | class LocalizationFormComponent extends Component {
method connectedCallback (line 22) | connectedCallback() {
method disconnectedCallback (line 34) | disconnectedCallback() {
method changeLanguage (line 107) | changeLanguage(event) {
method resizeLanguageInput (line 118) | resizeLanguageInput() {
method #findMatches (line 171) | #findMatches(
method #highlightMatches (line 230) | #highlightMatches(text, searchValue) {
method filterCountries (line 258) | filterCountries() {
method #changeCountryFocus (line 315) | #changeCountryFocus(direction) {
method resetCountriesFilter (line 342) | resetCountriesFilter(event) {
method resetForm (line 369) | resetForm() {
class DropdownLocalizationComponent (line 416) | class DropdownLocalizationComponent extends Component {
method isHidden (line 417) | get isHidden() {
method toggleSelector (line 424) | toggleSelector() {
method showPanel (line 431) | showPanel() {
method #updateWidth (line 474) | #updateWidth() {
class DrawerLocalizationComponent (line 503) | class DrawerLocalizationComponent extends Component {
method toggle (line 509) | toggle(event) {
FILE: assets/marquee.js
constant ANIMATION_OPTIONS (line 4) | const ANIMATION_OPTIONS = {
class MarqueeComponent (line 18) | class MarqueeComponent extends Component {
method connectedCallback (line 21) | async connectedCallback() {
method disconnectedCallback (line 41) | disconnectedCallback() {
method #speedUp (line 76) | #speedUp() {
method clonedContent (line 97) | get clonedContent() {
method #setSpeed (line 107) | #setSpeed(value) {
method #queryNumberOfCopies (line 111) | async #queryNumberOfCopies() {
method #calculateSpeed (line 148) | #calculateSpeed(numberOfCopies) {
method #restartAnimation (line 176) | #restartAnimation() {
method #duplicateContent (line 186) | #duplicateContent() {
method #addRepeatedItems (line 200) | #addRepeatedItems(numberOfCopies) {
method #removeRepeatedItems (line 214) | #removeRepeatedItems(numberOfCopies) {
function animateValue (line 237) | function animateValue({ from, to, duration, onUpdate, easing = (t) => t ...
FILE: assets/media-gallery.js
class MediaGallery (line 14) | class MediaGallery extends Component {
method connectedCallback (line 15) | connectedCallback() {
method disconnectedCallback (line 29) | disconnectedCallback() {
method zoom (line 65) | zoom(index, event) {
method preloadImage (line 73) | preloadImage(index) {
method slideshow (line 80) | get slideshow() {
method media (line 84) | get media() {
method presentation (line 88) | get presentation() {
FILE: assets/media.js
class DeferredMedia (line 13) | class DeferredMedia extends Component {
method connectedCallback (line 19) | connectedCallback() {
method disconnectedCallback (line 27) | disconnectedCallback() {
method updatePlayPauseHint (line 36) | updatePlayPauseHint(isPlaying) {
method loadContent (line 60) | loadContent(focus = true) {
method toggleMedia (line 87) | toggleMedia() {
method playMedia (line 95) | playMedia() {
method pauseMedia (line 115) | pauseMedia() {
class ProductModel (line 145) | class ProductModel extends DeferredMedia {
method loadContent (line 148) | loadContent() {
method disconnectedCallback (line 160) | disconnectedCallback() {
method pauseMedia (line 165) | pauseMedia() {
method playMedia (line 170) | playMedia() {
method setupModelViewerUI (line 178) | async setupModelViewerUI(errors) {
method #waitForModelViewerUI (line 233) | async #waitForModelViewerUI() {
FILE: assets/money-formatting.js
constant DEFAULT_CURRENCY_DECIMALS (line 12) | const DEFAULT_CURRENCY_DECIMALS = 2;
constant CURRENCY_DECIMALS (line 18) | const CURRENCY_DECIMALS = {
function convertMoneyToMinorUnits (line 73) | function convertMoneyToMinorUnits(value, currency) {
function formatCents (line 133) | function formatCents(moneyValue, thousandsSeparator, decimalSeparator, p...
function formatMoney (line 153) | function formatMoney(moneyValue, format, currency) {
FILE: assets/morph.js
constant HYDRATION_KEY_ATTRIBUTE (line 13) | const HYDRATION_KEY_ATTRIBUTE = 'data-hydration-key';
constant MORPH_OPTIONS (line 19) | const MORPH_OPTIONS = {
method reject (line 22) | reject(oldNode, newNode) {
method onBeforeUpdate (line 46) | onBeforeUpdate(oldNode, newNode) {
method onAfterUpdate (line 82) | onAfterUpdate(node) {
function morph (line 96) | function morph(oldTree, newTree, options = MORPH_OPTIONS) {
function collectHydrationTargets (line 133) | function collectHydrationTargets(root) {
function morphHydrationByKey (line 158) | function morphHydrationByKey(oldRoot, newRoot, options) {
function walk (line 198) | function walk(newNode, oldNode, options) {
function updateNode (line 245) | function updateNode(newNode, oldNode, options) {
function getNodeKey (line 294) | function getNodeKey(node, options) {
function updateAttribute (line 304) | function updateAttribute(newNode, oldNode, name) {
function copyAttributes (line 320) | function copyAttributes(newNode, oldNode) {
function updateInput (line 379) | function updateInput(newNode, oldNode) {
function updateTextarea (line 416) | function updateTextarea(newNode, oldNode) {
function recreateAppBlockScripts (line 436) | function recreateAppBlockScripts(container) {
function updateChildren (line 464) | function updateChildren(newNode, oldNode, options) {
function same (line 556) | function same(a, b, options) {
FILE: assets/overflow-list.js
class OverflowMinimumEvent (line 8) | class OverflowMinimumEvent extends Event {
method constructor (line 13) | constructor(minimumReached) {
class OverflowList (line 30) | class OverflowList extends DeclarativeShadowElement {
method observedAttributes (line 31) | static get observedAttributes() {
method attributeChangedCallback (line 40) | attributeChangedCallback(name, oldValue, newValue) {
method connectedCallback (line 50) | async connectedCallback() {
method #waitForStyles (line 60) | #waitForStyles() {
method #initialize (line 77) | #initialize() {
method disconnectedCallback (line 134) | disconnectedCallback() {
method schedule (line 140) | get schedule() {
method minimumItems (line 153) | get minimumItems() {
method overflowSlot (line 158) | get overflowSlot() {
method defaultSlot (line 163) | get defaultSlot() {
method #moveItemsToDefaultSlot (line 202) | #moveItemsToDefaultSlot() {
method #reset (line 215) | #reset() {
method #updateMinimumReached (line 229) | #updateMinimumReached(visibleElements) {
method showAll (line 246) | showAll() {
method #observeChanges (line 346) | #observeChanges() {
method #unobserveChanges (line 351) | #unobserveChanges() {
FILE: assets/paginated-list-aspect-ratio.js
class PaginatedListAspectRatioHelper (line 5) | class PaginatedListAspectRatioHelper {
method constructor (line 23) | constructor({ templateCard }) {
method processNewElements (line 31) | processNewElements() {
method #storeImageRatioSettings (line 43) | #storeImageRatioSettings(templateCard) {
method #fixAdaptiveAspectRatios (line 51) | #fixAdaptiveAspectRatios() {
method #applyFixedAspectRatio (line 96) | #applyFixedAspectRatio() {
method #getSafeImageAspectRatio (line 121) | #getSafeImageAspectRatio(width, height) {
method #getAspectRatioValue (line 131) | #getAspectRatioValue(ratioSetting) {
method #applyAspectRatioToGallery (line 140) | #applyAspectRatioToGallery(gallery, aspectRatio) {
method #getUnprocessedGalleries (line 159) | #getUnprocessedGalleries() {
method #markAsProcessed (line 167) | #markAsProcessed(gallery) {
FILE: assets/paginated-list.js
class PaginatedList (line 18) | class PaginatedList extends Component {
method connectedCallback (line 36) | connectedCallback() {
method disconnectedCallback (line 55) | disconnectedCallback() {
method #observeViewMore (line 64) | #observeViewMore() {
method #shouldUsePage (line 110) | #shouldUsePage(pageInfo) {
method #fetchPage (line 124) | async #fetchPage(type) {
method #fetchSpecificPage (line 152) | async #fetchSpecificPage(pageNumber, url = undefined) {
method #renderNextPage (line 167) | async #renderNextPage() {
method #renderPreviousPage (line 201) | async #renderPreviousPage() {
method #getPage (line 254) | #getPage(type) {
method #getGridForPage (line 281) | #getGridForPage(page) {
method sectionId (line 292) | get sectionId() {
FILE: assets/performance.js
class ThemePerformance (line 1) | class ThemePerformance {
method constructor (line 5) | constructor(metricPrefix) {
method createStartingMarker (line 13) | createStartingMarker(benchmarkName) {
method measureFromEvent (line 23) | measureFromEvent(benchmarkName, event) {
method measureFromMarker (line 38) | measureFromMarker(startMarker) {
method measure (line 50) | measure(benchmarkName, callback) {
FILE: assets/popover-polyfill.js
method constructor (line 14) | constructor(type, { oldState = '', newState = '', ...init } = {}) {
function queuePopoverToggleEventTask (line 21) | function queuePopoverToggleEventTask(element, oldState, newState) {
function getPopoverVisibilityState (line 44) | function getPopoverVisibilityState(popover) {
function lastSetElement (line 48) | function lastSetElement(set) {
function popoverTargetAttributeActivationBehavior (line 51) | function popoverTargetAttributeActivationBehavior(element) {
function checkPopoverValidity (line 68) | function checkPopoverValidity(element, expectedToBeShowing) {
function getStackPosition (line 85) | function getStackPosition(popover) {
function topMostClickedPopover (line 97) | function topMostClickedPopover(target) {
function topmostAutoOrHintPopover (line 105) | function topmostAutoOrHintPopover(document2) {
function topMostPopoverInList (line 120) | function topMostPopoverInList(list) {
function getRootNode (line 130) | function getRootNode(node) {
function nearestInclusiveOpenPopover (line 137) | function nearestInclusiveOpenPopover(node) {
function nearestInclusiveTargetPopoverForInvoker (line 147) | function nearestInclusiveTargetPopoverForInvoker(node) {
function topMostPopoverAncestor (line 156) | function topMostPopoverAncestor(newPopover, list) {
function isFocusable (line 190) | function isFocusable(focusTarget) {
function focusDelegate (line 211) | function focusDelegate(focusTarget) {
function popoverFocusingSteps (line 247) | function popoverFocusingSteps(subject) {
function showPopover (line 252) | function showPopover(element) {
function hidePopover (line 330) | function hidePopover(element, focusPreviousElement = false, fireEvents =...
function closeAllOpenPopovers (line 376) | function closeAllOpenPopovers(document2, focusPreviousElement = false, f...
function closeAllOpenPopoversInList (line 383) | function closeAllOpenPopoversInList(list, focusPreviousElement = false, ...
function hidePopoverStackUntil (line 390) | function hidePopoverStackUntil(endpoint, set, focusPreviousElement, fire...
function hideAllPopoversUntil (line 417) | function hideAllPopoversUntil(endpoint, focusPreviousElement, fireEvents) {
function lightDismissOpenPopovers (line 438) | function lightDismissOpenPopovers(event) {
function setInvokerAriaExpanded (line 457) | function setInvokerAriaExpanded(el, force = false) {
function isSupported (line 477) | function isSupported() {
function patchSelectorFn (line 484) | function patchSelectorFn(object, name, mapper) {
function hasLayerSupport (line 493) | function hasLayerSupport() {
function getStyles (line 496) | function getStyles() {
function injectStyles (line 563) | function injectStyles(root) {
function apply (line 585) | function apply() {
FILE: assets/predictive-search.js
class PredictiveSearchComponent (line 21) | class PredictiveSearchComponent extends Component {
method dialog (line 37) | get dialog() {
method connectedCallback (line 41) | connectedCallback() {
method disconnectedCallback (line 85) | disconnectedCallback() {
method #loadEmptyState (line 113) | #loadEmptyState() {
method #allResultsItems (line 119) | get #allResultsItems() {
method #currentIndex (line 146) | get #currentIndex() {
method #currentIndex (line 150) | set #currentIndex(index) {
method #currentItem (line 175) | get #currentItem() {
method clearRecentlyViewedProducts (line 246) | clearRecentlyViewedProducts(event) {
method #resetScrollPositions (line 302) | #resetScrollPositions() {
method #getSearchResults (line 313) | async #getSearchResults(searchTerm) {
method #getRecentlyViewedProductsMarkup (line 345) | async #getRecentlyViewedProductsMarkup() {
method #hideResetButton (line 358) | #hideResetButton() {
method #showResetButton (line 364) | #showResetButton() {
method #createAbortController (line 370) | #createAbortController() {
FILE: assets/price-per-item.js
class PricePerItemComponent (line 17) | class PricePerItemComponent extends Component {
method connectedCallback (line 22) | connectedCallback() {
method disconnectedCallback (line 29) | disconnectedCallback() {
method #parsePriceBreaks (line 37) | #parsePriceBreaks() {
method #attachEventListeners (line 63) | #attachEventListeners() {
method #getCurrentQuantity (line 94) | #getCurrentQuantity() {
method updatePriceDisplay (line 110) | updatePriceDisplay() {
FILE: assets/product-card.js
class ProductCardLink (line 22) | class ProductCardLink extends Component {
method productTransitionEnabled (line 23) | get productTransitionEnabled() {
method featuredMediaUrl (line 27) | get featuredMediaUrl() {
method handleViewTransition (line 35) | handleViewTransition(event) {
method #setImageSrcset (line 68) | #setImageSrcset(image) {
class ProductCard (line 105) | class ProductCard extends ProductCardLink {
method productPageUrl (line 108) | get productPageUrl() {
method getSelectedVariantId (line 116) | getSelectedVariantId() {
method getProductCardLink (line 128) | getProductCardLink() {
method connectedCallback (line 155) | connectedCallback() {
method disconnectedCallback (line 177) | disconnectedCallback() {
method #preloadNextPreviewImage (line 182) | #preloadNextPreviewImage() {
method #updateOverflowList (line 244) | #updateOverflowList() {
method updatePrice (line 266) | updatePrice(event) {
method #updateProductUrl (line 279) | #updateProductUrl(event) {
method #isUnavailableVariantSelected (line 310) | #isUnavailableVariantSelected(event) {
method #toggleAddToCartButton (line 324) | #toggleAddToCartButton(enable) {
method #updateVariantImages (line 335) | #updateVariantImages() {
method allVariants (line 360) | get allVariants() {
method variantPicker (line 368) | get variantPicker() {
method previewVariant (line 388) | previewVariant(id) {
method previewImage (line 401) | previewImage(event) {
method resetImage (line 422) | resetImage(event) {
class SwatchesVariantPickerComponent (line 536) | class SwatchesVariantPickerComponent extends VariantPicker {
method connectedCallback (line 537) | connectedCallback() {
method #handleCardVariantUrlUpdate (line 550) | #handleCardVariantUrlUpdate() {
method variantChanged (line 563) | variantChanged(event) {
method showAllSwatches (line 608) | showAllSwatches(event) {
FILE: assets/product-custom-property.js
class ProductCustomProperty (line 14) | class ProductCustomProperty extends Component {
method handleInput (line 15) | handleInput() {
method #updateCharacterCount (line 19) | #updateCharacterCount() {
FILE: assets/product-form.js
constant ERROR_MESSAGE_DISPLAY_DURATION (line 8) | const ERROR_MESSAGE_DISPLAY_DURATION = 10000;
constant ERROR_BUTTON_REENABLE_DELAY (line 11) | const ERROR_BUTTON_REENABLE_DELAY = 1000;
constant SUCCESS_MESSAGE_DISPLAY_DURATION (line 14) | const SUCCESS_MESSAGE_DISPLAY_DURATION = 5000;
class AddToCartComponent (line 31) | class AddToCartComponent extends Component {
method connectedCallback (line 37) | connectedCallback() {
method disconnectedCallback (line 43) | disconnectedCallback() {
method disable (line 55) | disable() {
method enable (line 62) | enable() {
method handleClick (line 70) | handleClick(event) {
method #animateFlyToCart (line 104) | #animateFlyToCart() {
class ProductFormComponent (line 191) | class ProductFormComponent extends Component {
method connectedCallback (line 204) | connectedCallback() {
method disconnectedCallback (line 216) | disconnectedCallback() {
method #updateCartQuantityFromData (line 227) | #updateCartQuantityFromData(cart) {
method #fetchAndUpdateCartQuantity (line 250) | async #fetchAndUpdateCartQuantity() {
method handleSubmit (line 282) | handleSubmit(event) {
method #getIntendedVariantId (line 301) | #getIntendedVariantId() {
method #getQuantity (line 306) | #getQuantity() {
method #processAddToCart (line 315) | #processAddToCart(overrideVariantId, overrideQuantity, event) {
method #processBatchAddToCart (line 492) | #processBatchAddToCart(items) {
method #updateQuantityLabel (line 594) | #updateQuantityLabel(cartQty) {
method #setLiveRegionText (line 610) | #setLiveRegionText(text) {
method #clearLiveRegionText (line 615) | #clearLiveRegionText() {
method #morphOrUpdateElement (line 626) | #morphOrUpdateElement(currentElement, newElement, insertReferenceEleme...
FILE: assets/product-hotspot.js
class ProductHotspotComponent (line 16) | class ProductHotspotComponent extends Component {
method connectedCallback (line 22) | connectedCallback() {
method disconnectedCallback (line 32) | disconnectedCallback() {
method #openQuickAddModal (line 44) | #openQuickAddModal() {
method #setupDesktopListeners (line 55) | #setupDesktopListeners() {
method #removeDesktopListeners (line 77) | #removeDesktopListeners() {
method #calculateDialogPlacement (line 111) | #calculateDialogPlacement() {
method getHotspotProductLink (line 253) | getHotspotProductLink() {
FILE: assets/product-inventory.js
class ProductInventory (line 5) | class ProductInventory extends Component {
method connectedCallback (line 6) | connectedCallback() {
method disconnectedCallback (line 12) | disconnectedCallback() {
FILE: assets/product-price.js
class ProductPrice (line 19) | class ProductPrice extends Component {
method connectedCallback (line 20) | connectedCallback() {
method disconnectedCallback (line 27) | disconnectedCallback() {
FILE: assets/product-recommendations.js
class ProductRecommendations (line 3) | class ProductRecommendations extends Component {
method connectedCallback (line 58) | connectedCallback() {
method disconnectedCallback (line 64) | disconnectedCallback() {
method #loadRecommendations (line 73) | #loadRecommendations() {
method #fetchCachedRecommendations (line 121) | async #fetchCachedRecommendations(productId, sectionId, intent) {
method #handleError (line 150) | #handleError(error) {
FILE: assets/product-sku.js
class ProductSkuComponent (line 17) | class ProductSkuComponent extends Component {
method connectedCallback (line 20) | connectedCallback() {
method disconnectedCallback (line 27) | disconnectedCallback() {
FILE: assets/product-title-truncation.js
class ProductTitle (line 13) | class ProductTitle extends Component {
method constructor (line 14) | constructor() {
method connectedCallback (line 18) | connectedCallback() {
method #initializeTruncation (line 26) | #initializeTruncation() {
method #calculateTruncation (line 44) | #calculateTruncation() {
method #handleResize (line 69) | #handleResize() {
method disconnectedCallback (line 73) | disconnectedCallback() {
FILE: assets/qr-code-generator.js
function QR8bitByte (line 54) | function QR8bitByte(data) {
function QRCodeModel (line 114) | function QRCodeModel(typeNumber, errorCorrectLevel) {
function QRPolynomial (line 808) | function QRPolynomial(num, shift) {
function QRRSBlock (line 872) | function QRRSBlock(totalCount, dataCount) {
function QRBitBuffer (line 1083) | function QRBitBuffer() {
function isSupportCanvas (line 1175) | function isSupportCanvas() {
function makeSVG (line 1204) | function makeSVG(tag, attrs) {
function onMakeImage (line 1322) | function onMakeImage() {
function getTypeNumber (line 1484) | function getTypeNumber(sText, nCorrectLevel) {
function getUTF8Length (line 1534) | function getUTF8Length(sText) {
function QRCode (line 1620) | function QRCode(el, vOption) {
FILE: assets/qr-code-image.js
class QRCodeImage (line 8) | class QRCodeImage extends Component {
method connectedCallback (line 16) | connectedCallback() {
FILE: assets/quick-add.js
class QuickAddComponent (line 8) | class QuickAddComponent extends Component {
method productPageUrl (line 16) | get productPageUrl() {
method #getSelectedVariantId (line 43) | #getSelectedVariantId() {
method connectedCallback (line 48) | connectedCallback() {
method disconnectedCallback (line 58) | disconnectedCallback() {
method #updateVariantPicker (line 78) | #updateVariantPicker(newHtml) {
method #resetScroll (line 120) | #resetScroll() {
method #stayVisibleUntilDialogCloses (line 131) | #stayVisibleUntilDialogCloses(dialogComponent) {
method fetchProductPage (line 166) | async fetchProductPage(productPageUrl) {
method updateQuickAddModal (line 201) | async updateQuickAddModal(productGrid) {
method #updateQuickAddButtonState (line 245) | #updateQuickAddButtonState(event) {
method #syncVariantSelection (line 257) | #syncVariantSelection(modalContent) {
class QuickAddDialog (line 277) | class QuickAddDialog extends DialogComponent {
method connectedCallback (line 280) | connectedCallback() {
method disconnectedCallback (line 289) | disconnectedCallback() {
FILE: assets/quick-order-list.js
class QuickOrderListComponent (line 21) | class QuickOrderListComponent extends Component {
method currentPage (line 45) | get currentPage() {
method cartVariantIds (line 59) | get cartVariantIds() {
method connectedCallback (line 66) | connectedCallback() {
method disconnectedCallback (line 78) | disconnectedCallback() {
method #isQuantityInput (line 93) | #isQuantityInput(target) {
method #scrollToCenter (line 147) | #scrollToCenter(element) {
method onPaginationControlClick (line 159) | async onPaginationControlClick(data, event) {
method onLineItemRemove (line 184) | async onLineItemRemove(variantId, event) {
method onRemoveAll (line 201) | async onRemoveAll(event) {
method #handleQuantityUpdate (line 272) | async #handleQuantityUpdate(event) {
method #handleCartUpdate (line 368) | async #handleCartUpdate(event) {
method #disableQuickOrderListItems (line 397) | #disableQuickOrderListItems() {
method #enableQuickOrderListItems (line 401) | #enableQuickOrderListItems() {
method showRemoveAllConfirmation (line 409) | showRemoveAllConfirmation(event) {
method hideRemoveAllConfirmation (line 418) | hideRemoveAllConfirmation(event) {
method #toggleConfirmationPanel (line 427) | #toggleConfirmationPanel(show) {
method #showErrorMessage (line 436) | #showErrorMessage(message) {
method #clearErrorMessage (line 444) | #clearErrorMessage() {
method #showSuccessMessage (line 452) | #showSuccessMessage(quantityAdded) {
method #clearSuccessMessage (line 464) | #clearSuccessMessage() {
method #applyShimmerEffects (line 472) | #applyShimmerEffects(variantIds) {
method #scrollToTopOfSection (line 489) | #scrollToTopOfSection() {
method #updateSectionHTML (line 501) | #updateSectionHTML(data) {
method #getSectionIds (line 514) | #getSectionIds() {
FILE: assets/recently-viewed-products.js
class RecentlyViewed (line 4) | class RecentlyViewed {
method addProduct (line 14) | static addProduct(productId) {
method clearProducts (line 24) | static clearProducts() {
method getProducts (line 32) | static getProducts() {
FILE: assets/results-list.js
class ResultsList (line 7) | class ResultsList extends PaginatedList {
method connectedCallback (line 8) | connectedCallback() {
method disconnectedCallback (line 15) | disconnectedCallback() {
method updateLayout (line 24) | updateLayout({ target }) {
method #setLayout (line 53) | #setLayout(value) {
FILE: assets/rte-formatter.js
class RTEFormatter (line 6) | class RTEFormatter extends Component {
method connectedCallback (line 7) | connectedCallback() {
method #formatTable (line 16) | #formatTable(table) {
FILE: assets/scrolling.js
constant SCROLL_END_TIMEOUT (line 7) | const SCROLL_END_TIMEOUT = 50;
class Scroller (line 14) | class Scroller {
method constructor (line 84) | constructor(element, options) {
method to (line 101) | async to(input, options) {
method by (line 129) | by(value, options) {
method #scroll (line 140) | #scroll(options) {
method axis (line 170) | get axis() {
method finished (line 179) | get finished() {
method #edge (line 187) | get #edge() {
method #setup (line 194) | #setup() {
method snap (line 258) | set snap(value) {
method destroy (line 267) | destroy() {
function getScrollAxis (line 277) | function getScrollAxis(el) {
function calculatePaddingStart (line 295) | function calculatePaddingStart(element, axis) {
function scrollIntoView (line 311) | function scrollIntoView(element, { ancestor, behavior = 'smooth', block ...
class ScrollHint (line 370) | class ScrollHint extends HTMLElement {
method connectedCallback (line 374) | connectedCallback() {
method disconnectedCallback (line 379) | disconnectedCallback() {
FILE: assets/search-page-input.js
class SearchPageInputComponent (line 11) | class SearchPageInputComponent extends Component {
method #submitEmptySearch (line 28) | #submitEmptySearch() {
FILE: assets/section-hydration.js
function hydrateSection (line 11) | async function hydrateSection(sectionId, url) {
function hydrate (line 31) | async function hydrate(sectionId, url) {
FILE: assets/section-renderer.js
class SectionRenderer (line 6) | class SectionRenderer {
method constructor (line 25) | constructor() {
method renderSection (line 38) | async renderSection(sectionId, options) {
method #abortPendingMorph (line 61) | #abortPendingMorph(sectionId) {
method getSectionHTML (line 75) | async getSectionHTML(sectionId, useCache = true, url = new URL(window....
method #cachePageSections (line 103) | #cachePageSections() {
constant SECTION_ID_PREFIX (line 114) | const SECTION_ID_PREFIX = 'shopify-section-';
function buildSectionRenderingURL (line 122) | function buildSectionRenderingURL(sectionId, url = new URL(window.locati...
function buildSectionSelector (line 134) | function buildSectionSelector(sectionId) {
function normalizeSectionId (line 143) | function normalizeSectionId(sectionId) {
function containsShadowRoot (line 152) | function containsShadowRoot(element) {
function morphSection (line 167) | async function morphSection(sectionId, html, mode = 'full') {
FILE: assets/show-more.js
class ShowMoreComponent (line 17) | class ShowMoreComponent extends Component {
method #currentBreakpoint (line 43) | get #currentBreakpoint() {
method connectedCallback (line 57) | connectedCallback() {
method #onAnimationFinish (line 127) | #onAnimationFinish() {
FILE: assets/slideshow.js
constant SLIDE_VISIBLITY_THRESHOLD (line 16) | const SLIDE_VISIBLITY_THRESHOLD = 0.7;
class SlideshowViewportObserver (line 26) | class SlideshowViewportObserver {
method getInstance (line 37) | static getInstance() {
method observe (line 48) | observe(slideshow) {
method unobserve (line 74) | unobserve(slideshow) {
class Slideshow (line 95) | class Slideshow extends Component {
method observedAttributes (line 96) | static get observedAttributes() {
method attributeChangedCallback (line 105) | attributeChangedCallback(name, oldValue, newValue) {
method connectedCallback (line 125) | async connectedCallback() {
method disconnectedCallback (line 143) | disconnectedCallback() {
method isNested (line 174) | get isNested() {
method initialSlide (line 178) | get initialSlide() {
method select (line 189) | async select(input, event, options = {}) {
method next (line 323) | next(event, options) {
method previous (line 334) | previous(event, options) {
method play (line 343) | play(interval = this.autoplayInterval) {
method pause (line 358) | pause() {
method paused (line 363) | get paused() {
method paused (line 367) | set paused(value) {
method suspend (line 378) | suspend() {
method resume (line 386) | resume() {
method autoplay (line 393) | get autoplay() {
method autoplayInterval (line 397) | get autoplayInterval() {
method current (line 412) | get current() {
method current (line 420) | set current(value) {
method infinite (line 435) | get infinite() {
method visibleSlides (line 439) | get visibleSlides() {
method previousIndex (line 443) | get previousIndex() {
method nextIndex (line 450) | get nextIndex() {
method atStart (line 457) | get atStart() {
method atEnd (line 463) | get atEnd() {
method disabled (line 473) | set disabled(value) {
method disabled (line 480) | get disabled() {
method #setupSlideshowWithoutControls (line 525) | #setupSlideshowWithoutControls() {
method #setupSlideshow (line 542) | #setupSlideshow() {
method slides (line 819) | get slides() {
method initialSlideIndex (line 827) | get initialSlideIndex() {
method #updateControlsVisibility (line 839) | #updateControlsVisibility() {
method #setupIntersectionObserver (line 852) | #setupIntersectionObserver() {
method #centerSelectedThumbnail (line 904) | #centerSelectedThumbnail(index, behavior = 'smooth') {
method #updateVisibleSlides (line 922) | #updateVisibleSlides() {
FILE: assets/sticky-add-to-cart.js
class StickyAddToCartComponent (line 41) | class StickyAddToCartComponent extends Component {
method connectedCallback (line 71) | connectedCallback() {
method disconnectedCallback (line 88) | disconnectedCallback() {
method #setupIntersectionObserver (line 101) | #setupIntersectionObserver() {
method #showStickyBar (line 296) | #showStickyBar() {
method #hideStickyBar (line 305) | #hideStickyBar() {
method #getProductForm (line 316) | #getProductForm() {
method #getInitialQuantity (line 332) | #getInitialQuantity() {
method #updateButtonText (line 340) | #updateButtonText() {
FILE: assets/theme-editor.js
function wasPageUnloading (line 174) | function wasPageUnloading() {
function clearAllEditorStates (line 186) | function clearAllEditorStates() {
function getEditorState (line 198) | function getEditorState(name) {
function saveEditorState (line 208) | function saveEditorState(name, isOpen, instanceId) {
method matches (line 217) | matches(el) {
method matches (line 226) | matches(el) {
method matches (line 236) | matches(el) {
method matches (line 246) | matches(el) {
method matches (line 256) | matches(el) {
method matches (line 268) | matches(el) {
method matches (line 282) | matches(el) {
method matches (line 298) | matches(el) {
method matches (line 317) | matches(el) {
method matches (line 326) | matches(el) {
FILE: assets/utilities.js
function isLowPowerDevice (line 29) | function isLowPowerDevice() {
function supportsViewTransitions (line 37) | function supportsViewTransitions() {
function startViewTransition (line 92) | function startViewTransition(callback, types) {
function fetchConfig (line 147) | function fetchConfig(type = 'json', config = {}) {
function debounce (line 174) | function debounce(fn, wait) {
function throttle (line 200) | function throttle(fn, delay) {
function prefersReducedMotion (line 230) | function prefersReducedMotion() {
function normalizeString (line 239) | function normalizeString(str) {
function onDocumentLoaded (line 250) | function onDocumentLoaded(callback) {
function onDocumentReady (line 263) | function onDocumentReady(callback) {
function removeWillChangeOnAnimationEnd (line 276) | function removeWillChangeOnAnimationEnd(event) {
function onAnimationEnd (line 291) | function onAnimationEnd(elements, callback, options = { subtree: true }) {
function isClickedOutside (line 313) | function isClickedOutside(event, element) {
function isPointWithinElement (line 328) | function isPointWithinElement(x, y, element) {
function isMobileBreakpoint (line 344) | function isMobileBreakpoint() {
function isDesktopBreakpoint (line 352) | function isDesktopBreakpoint() {
function isTouchDevice (line 360) | function isTouchDevice() {
function clamp (line 371) | function clamp(value, min, max) {
function center (line 383) | function center(element, axis) {
function start (line 401) | function start(element, axis) {
function closest (line 416) | function closest(values, target) {
function preventDefault (line 426) | function preventDefault(event) {
function getVisibleElements (line 439) | function getVisibleElements(root, elements, ratio = 1, axis) {
function getIOSVersion (line 484) | function getIOSVersion() {
function getCardsToAnimate (line 510) | function getCardsToAnimate(grid, cards) {
function preloadImage (line 563) | function preloadImage(src) {
class TextComponent (line 568) | class TextComponent extends HTMLElement {
method shimmer (line 569) | shimmer() {
function resetShimmer (line 582) | function resetShimmer(container = document.body) {
function changeMetaThemeColor (line 591) | function changeMetaThemeColor(color) {
function getViewParameterValue (line 604) | function getViewParameterValue() {
function parseIntOrDefault (line 616) | function parseIntOrDefault(value, defaultValue) {
class Scheduler (line 624) | class Scheduler {
function oncePerEditorSession (line 663) | function oncePerEditorSession(element, sessionKeyName, callback) {
class ResizeNotifier (line 683) | class ResizeNotifier extends ResizeObserver {
method constructor (line 689) | constructor(callback) {
method disconnect (line 696) | disconnect() {
function setHeaderMenuStyle (line 705) | function setHeaderMenuStyle() {
function calculateHeaderGroupHeight (line 722) | function calculateHeaderGroupHeight(
function updateTransparentHeaderOffset (line 748) | function updateTransparentHeaderOffset() {
function updateHeaderHeights (line 766) | function updateHeaderHeights() {
function updateAllHeaderCustomProperties (line 787) | function updateAllHeaderCustomProperties() {
FILE: assets/variant-picker.js
class VariantPicker (line 17) | class VariantPicker extends Component {
method connectedCallback (line 32) | connectedCallback() {
method disconnectedCallback (line 50) | disconnectedCallback() {
method variantChanged (line 59) | variantChanged(event) {
method #getFieldsetMeasurements (line 130) | #getFieldsetMeasurements(fieldsetIndex) {
method #applyFieldsetMeasurements (line 153) | #applyFieldsetMeasurements({ fieldset, currentWidth, previousWidth, cu...
method updateFieldsetCss (line 171) | updateFieldsetCss(fieldsetIndex) {
method updateSelectedOption (line 184) | updateSelectedOption(target) {
method buildRequestUrl (line 259) | buildRequestUrl(selectedOption, source = null, sourceSelectedOptionsVa...
method fetchUpdatedSection (line 306) | fetchUpdatedSection(requestUrl, morphElementSelector) {
method updateVariantPicker (line 363) | updateVariantPicker(newHtml) {
method updateVariantPickerCss (line 399) | updateVariantPickerCss() {
method updateElement (line 416) | updateElement(newHtml, elementSelector) {
method updateMain (line 431) | updateMain(newHtml) {
method selectedOption (line 446) | get selectedOption() {
method selectedOptionId (line 460) | get selectedOptionId() {
method selectedOptionsValues (line 476) | get selectedOptionsValues() {
FILE: assets/video-background.js
class VideoBackgroundComponent (line 12) | class VideoBackgroundComponent extends Component {
method connectedCallback (line 15) | connectedCallback() {
FILE: assets/view-transitions.js
function shouldSkipViewTransition (line 93) | function shouldSkipViewTransition(viewTransition) {
function isLowPowerDevice (line 100) | function isLowPowerDevice() {
FILE: assets/volume-pricing-info.js
class VolumePricingInfoComponent (line 13) | class VolumePricingInfoComponent extends Component {
method connectedCallback (line 17) | connectedCallback() {
method updatedCallback (line 38) | updatedCallback() {
method updateActiveTier (line 50) | updateActiveTier(quantity) {
FILE: assets/volume-pricing.js
class VolumePricingComponent (line 9) | class VolumePricingComponent extends Component {
method toggleExpanded (line 13) | toggleExpanded() {
FILE: assets/zoom-dialog.js
class ZoomDialog (line 24) | class ZoomDialog extends Component {
method connectedCallback (line 29) | connectedCallback() {
method disconnectedCallback (line 34) | disconnectedCallback() {
method open (line 45) | async open(index, event) {
method loadHighResolutionImage (line 89) | loadHighResolutionImage(mediaContainer) {
method close (line 139) | async close() {
method closeDialog (line 182) | closeDialog() {
method handleKeyDown (line 193) | handleKeyDown(event) {
method handleThumbnailClick (line 204) | async handleThumbnailClick(index) {
method handleThumbnailPointerEnter (line 213) | async handleThumbnailPointerEnter(index) {
method selectThumbnail (line 226) | async selectThumbnail(index, options = { behavior: 'smooth' }) {
function getMostVisibleElement (line 270) | function getMostVisibleElement(elements) {
Condensed preview — 425 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (5,198K chars).
[
{
"path": ".cursor/rules/accordion-accessibility.mdc",
"chars": 7381,
"preview": "---\ndescription: Accordion component accessibility compliance and WAI-ARIA Accordion Pattern\nglobs: *.vue, *.jsx, *.tsx,"
},
{
"path": ".cursor/rules/animation-accessibility.mdc",
"chars": 12978,
"preview": "---\ndescription: Enforce animation accessibility standards per WCAG 2.2.2 Pause Stop Hide, 2.3.1 Three Flashes or Below "
},
{
"path": ".cursor/rules/assets.mdc",
"chars": 623,
"preview": "---\ndescription: Static files (css, js, and images) for theme templates\nglobs: assets/*\nalwaysApply: false\n---\n# Assets\n"
},
{
"path": ".cursor/rules/blocks.mdc",
"chars": 10188,
"preview": "---\ndescription: Development standards and best practices for creating/configuring/styling theme blocks, including stati"
},
{
"path": ".cursor/rules/breadcrumb-accessibility.mdc",
"chars": 3962,
"preview": "---\ndescription: Breadcrumb component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, "
},
{
"path": ".cursor/rules/carousel-accessibility.mdc",
"chars": 18588,
"preview": "---\ndescription: Carousel component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *."
},
{
"path": ".cursor/rules/cart-drawer-accessibility.mdc",
"chars": 11465,
"preview": "---\ndescription: Cart drawer component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js,"
},
{
"path": ".cursor/rules/chat-window-accessibility.mdc",
"chars": 40772,
"preview": "---\ndescription: Chat window component accessibility compliance and WCAG compliance for real-time communication features"
},
{
"path": ".cursor/rules/color-contrast-accessibility.mdc",
"chars": 10543,
"preview": "---\ndescription: Text and user interface color contrast compliance with WCAG 2.2 1.4.3 and 1.4.11\nglobs: *.css, *.scss, "
},
{
"path": ".cursor/rules/color-swatch-accessibility.mdc",
"chars": 7158,
"preview": "---\ndescription: Color swatch component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js"
},
{
"path": ".cursor/rules/combobox-accessibility.mdc",
"chars": 9832,
"preview": "---\ndescription: Combobox component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *."
},
{
"path": ".cursor/rules/css-standards.mdc",
"chars": 22345,
"preview": "---\ndescription: Writing CSS, whether inside .css files or in the `{% stylesheet %}…{% endstylesheet %}` or `{% style %}"
},
{
"path": ".cursor/rules/disclosure-accessibility.mdc",
"chars": 4888,
"preview": "---\ndescription: Disclosure component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, "
},
{
"path": ".cursor/rules/dropdown-navigation-accessibility.mdc",
"chars": 15184,
"preview": "---\ndescription: Dropdown Navigation component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.ph"
},
{
"path": ".cursor/rules/examples/block-example-group.liquid",
"chars": 2555,
"preview": "{% doc %}\n Renders a group of blocks with configurable layout direction, gap and\n alignment.\n\n All settings apply to "
},
{
"path": ".cursor/rules/examples/block-example-text.liquid",
"chars": 1251,
"preview": "{% doc %}\n Renders a text block.\n\n @example\n {% content_for 'block', type: 'text', id: 'text' %}\n{% enddoc %}\n\n<div\n "
},
{
"path": ".cursor/rules/examples/section-example.liquid",
"chars": 1231,
"preview": "<div class=\"example-section full-width\">\n {% if section.settings.background_image %}\n <div class=\"example-section__b"
},
{
"path": ".cursor/rules/examples/snippet-example.liquid",
"chars": 2528,
"preview": "{% doc %}\n Product Card Snippet Template\n\n @param product - {Object} Product object (required)\n @param show_vendor - "
},
{
"path": ".cursor/rules/flip-card-accessibility.mdc",
"chars": 13697,
"preview": "---\ndescription: Flip Card component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *"
},
{
"path": ".cursor/rules/focus-order-and-styles-accessibility.mdc",
"chars": 20937,
"preview": "---\ndescription: Focus order and focus styles accessibility standards per WCAG 2.4.7 Focus Visible, 1.4.11 Non-Text Cont"
},
{
"path": ".cursor/rules/form-accessibility.mdc",
"chars": 26932,
"preview": "---\ndescription: Form component accessibility standards and WCAG compliance for form inputs, labels, instructions, and e"
},
{
"path": ".cursor/rules/global-accessibility-standards.mdc",
"chars": 13728,
"preview": "---\ndescription: Global scope accessibility standards per WCAG requirements for page language, viewport, title attribute"
},
{
"path": ".cursor/rules/heading-accessibility.mdc",
"chars": 9844,
"preview": "---\ndescription: Heading element accessibility compliance and WCAG 2.4.1 Bypass Blocks requirements\nglobs: *.vue, *.jsx,"
},
{
"path": ".cursor/rules/html-standards.mdc",
"chars": 9012,
"preview": "---\ndescription:\nglobs: *.liquid\nalwaysApply: false\n---\n# Modern HTML Standards\n\nUse the latest evergreen browser featur"
},
{
"path": ".cursor/rules/image-alt-text-accessibility.mdc",
"chars": 14429,
"preview": "---\ndescription: Image alternative text accessibility compliance and WCAG 2.2 requirements\nglobs: *.vue, *.jsx, *.tsx, *"
},
{
"path": ".cursor/rules/javascript-standards.mdc",
"chars": 23911,
"preview": "---\ndescription: Writing JavaScript inside `.js` files, or within the `{% javascript %}` or `{% script %}` tags in `.liq"
},
{
"path": ".cursor/rules/landmark-accessibility.mdc",
"chars": 12831,
"preview": "---\ndescription: Landmark element accessibility compliance and WCAG 2.4.1 Bypass Blocks requirements\nglobs: *.vue, *.jsx"
},
{
"path": ".cursor/rules/liquid.mdc",
"chars": 6324,
"preview": "---\ndescription: Liquid syntax standards\nglobs: *.liquid\nalwaysApply: false\n---\n\n# Liquid Syntax Standards\n\n## ⚠️ CRITIC"
},
{
"path": ".cursor/rules/locales.mdc",
"chars": 1497,
"preview": "---\ndescription: Locales coding standards and best practices guide\nglobs: locales/*.json\nalwaysApply: false\n---\n# Transl"
},
{
"path": ".cursor/rules/localization.mdc",
"chars": 1721,
"preview": "---\ndescription: Localization coding standards and best practices guide\nglobs: *.liquid,schemas/*\nalwaysApply: false\n---"
},
{
"path": ".cursor/rules/mobile-accessibility-standards.mdc",
"chars": 16391,
"preview": "---\ndescription: Mobile accessibility standards per WCAG 2.5.8 Target Size (Minimum), 2.4.1 Bypass Blocks, and 1.3.4 Ori"
},
{
"path": ".cursor/rules/modal-accessibility.mdc",
"chars": 4920,
"preview": "---\ndescription: Modal window accessibility compliance and ARIA Dialog Pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php"
},
{
"path": ".cursor/rules/product-card-accessibility.mdc",
"chars": 10702,
"preview": "---\ndescription: Product card accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *."
},
{
"path": ".cursor/rules/product-filter-accessibility.mdc",
"chars": 17740,
"preview": "---\ndescription: Product filter component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *."
},
{
"path": ".cursor/rules/product-media-gallery-accessibility.mdc",
"chars": 24460,
"preview": "---\ndescription: Enforce product media gallery component accessibility standards and proper landmark structure for media"
},
{
"path": ".cursor/rules/prompts-and-references.mdc",
"chars": 2158,
"preview": "---\ndescription:\nglobs:\nalwaysApply: true\n---\n# Prompts and References\n\nThe prompts and references directories contain d"
},
{
"path": ".cursor/rules/sale-price-accessibility.mdc",
"chars": 7332,
"preview": "---\ndescription: Sale price component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, "
},
{
"path": ".cursor/rules/schemas.mdc",
"chars": 4862,
"preview": "---\ndescription:\nglobs: blocks/*.liquid,sections/*.liquid,schemas/*\nalwaysApply: false\n---\n\n# Schema Standards\n\nEvery se"
},
{
"path": ".cursor/rules/sections.mdc",
"chars": 1717,
"preview": "---\ndescription: Section coding standards and best practices guide\nglobs: sections/*.liquid\nalwaysApply: false\n---\n# Sec"
},
{
"path": ".cursor/rules/slider-accessibility.mdc",
"chars": 6433,
"preview": "---\ndescription: Slider component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts"
},
{
"path": ".cursor/rules/snippets.mdc",
"chars": 3540,
"preview": "---\ndescription:\nglobs: snippets/*.liquid\nalwaysApply: false\n---\n# Snippet Development Standards\n\n## Snippet Documentati"
},
{
"path": ".cursor/rules/switch-accessibility.mdc",
"chars": 5281,
"preview": "---\ndescription: Switch component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts"
},
{
"path": ".cursor/rules/tab-accessibility.mdc",
"chars": 6175,
"preview": "---\ndescription: Tab component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *"
},
{
"path": ".cursor/rules/table-accessibility.mdc",
"chars": 11363,
"preview": "---\ndescription: Table element accessibility compliance\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.ts, *.liquid\n"
},
{
"path": ".cursor/rules/templates.mdc",
"chars": 3356,
"preview": "---\ndescription:\nglobs: templates/*.json\nalwaysApply: false\n---\n# Templates\n\nAll JSON templates must follow this exact s"
},
{
"path": ".cursor/rules/theme-settings.mdc",
"chars": 1024,
"preview": "---\ndescription: Guidelines and examples for organizing and structuring the Shopify theme settings schema.\nglobs: config"
},
{
"path": ".cursor/rules/tooltip-accessibility.mdc",
"chars": 5023,
"preview": "---\ndescription: Tooltip component accessibility compliance pattern\nglobs: *.vue, *.jsx, *.tsx, *.html, *.php, *.js, *.t"
},
{
"path": "LICENSE.md",
"chars": 2279,
"preview": "Copyright (c) 2025-present Shopify Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of"
},
{
"path": "README.md",
"chars": 5160,
"preview": "# Horizon\n\n[Getting started](#getting-started) |\n[Staying up to date with Horizon changes](#staying-up-to-date-with-hori"
},
{
"path": "assets/accordion-custom.js",
"chars": 2856,
"preview": "import { mediaQueryLarge, isMobileBreakpoint } from '@theme/utilities';\n\n// Accordion\n// Still extends HTMLElement over "
},
{
"path": "assets/anchored-popover.js",
"chars": 4937,
"preview": "import { Component } from '@theme/component';\nimport { debounce, requestIdleCallback } from '@theme/utilities';\n\n/**\n * "
},
{
"path": "assets/announcement-bar.js",
"chars": 2809,
"preview": "import { Component } from '@theme/component';\n\n/**\n * Announcement banner custom element that allows fading between cont"
},
{
"path": "assets/auto-close-details.js",
"chars": 593,
"preview": "(function autoCloseDetails() {\n document.addEventListener('click', function (event) {\n const detailsToClose = [...do"
},
{
"path": "assets/base.css",
"chars": 123467,
"preview": "* {\n box-sizing: border-box;\n}\n\nbody {\n color: var(--color-foreground);\n background: var(--color-background);\n displ"
},
{
"path": "assets/blog-posts-list.js",
"chars": 289,
"preview": "import PaginatedList from '@theme/paginated-list';\n\n/**\n * A custom element that renders a paginated blog posts list\n */"
},
{
"path": "assets/cart-discount.js",
"chars": 6939,
"preview": "import { Component } from '@theme/component';\nimport { morphSection } from '@theme/section-renderer';\nimport { DiscountU"
},
{
"path": "assets/cart-drawer.js",
"chars": 3832,
"preview": "import { DialogComponent, DialogOpenEvent, DialogCloseEvent } from '@theme/dialog';\nimport { CartAddEvent } from '@theme"
},
{
"path": "assets/cart-icon.js",
"chars": 4232,
"preview": "import { Component } from '@theme/component';\nimport { onAnimationEnd } from '@theme/utilities';\nimport { ThemeEvents, C"
},
{
"path": "assets/cart-note.js",
"chars": 1219,
"preview": "import { Component } from '@theme/component';\nimport { debounce, fetchConfig } from '@theme/utilities';\nimport { cartPer"
},
{
"path": "assets/collection-links.js",
"chars": 6079,
"preview": "import { Component } from '@theme/component';\nimport { closest, clamp, center, getVisibleElements } from '@theme/utiliti"
},
{
"path": "assets/comparison-slider.js",
"chars": 4487,
"preview": "import { Component } from '@theme/component';\nimport { oncePerEditorSession } from '@theme/utilities';\n\n/**\n * Compariso"
},
{
"path": "assets/component-cart-items.js",
"chars": 10248,
"preview": "import { Component } from '@theme/component';\nimport {\n fetchConfig,\n debounce,\n onAnimationEnd,\n prefersReducedMoti"
},
{
"path": "assets/component-cart-quantity-selector.js",
"chars": 1468,
"preview": "import { QuantitySelectorComponent } from '@theme/component-quantity-selector';\n\n/**\n * A custom element that allows the"
},
{
"path": "assets/component-quantity-selector.js",
"chars": 8962,
"preview": "import { Component } from '@theme/component';\nimport { QuantitySelectorUpdateEvent } from '@theme/events';\nimport { pars"
},
{
"path": "assets/component.js",
"chars": 9650,
"preview": "import { requestIdleCallback } from '@theme/utilities';\n\n/*\n * Declarative shadow DOM is only initialized on the initial"
},
{
"path": "assets/copy-to-clipboard.js",
"chars": 742,
"preview": "import { Component } from '@theme/component';\n\n/**\n * Handles copying text to clipboard, from an event like a click.\n * "
},
{
"path": "assets/dialog.js",
"chars": 4571,
"preview": "import { Component } from '@theme/component';\nimport { debounce, isClickedOutside, onAnimationEnd } from '@theme/utiliti"
},
{
"path": "assets/drag-zoom-wrapper.js",
"chars": 16150,
"preview": "import { DialogCloseEvent } from './dialog.js';\nimport { clamp, preventDefault, isMobileBreakpoint } from './utilities.j"
},
{
"path": "assets/events.js",
"chars": 9561,
"preview": "/**\n * @namespace ThemeEvents\n * @description A collection of theme-specific events that can be used to trigger and list"
},
{
"path": "assets/facets.js",
"chars": 26262,
"preview": "import { sectionRenderer } from '@theme/section-renderer';\nimport { Component } from '@theme/component';\nimport { Filter"
},
{
"path": "assets/floating-panel.js",
"chars": 1839,
"preview": "import { debounce, requestIdleCallback, viewTransition } from '@theme/utilities';\nimport { Component } from '@theme/comp"
},
{
"path": "assets/fly-to-cart.js",
"chars": 2566,
"preview": "import { yieldToMainThread } from '@theme/utilities';\nimport { Component } from '@theme/component';\n\n/**\n * FlyToCart cu"
},
{
"path": "assets/focus.js",
"chars": 3336,
"preview": "// Store references to our event handlers so we can remove them.\n/** @type {Record<string, (event: Event) => void>} */\nc"
},
{
"path": "assets/gift-card-recipient-form.js",
"chars": 13524,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents, CartErrorEvent, CartAddEvent } from '@theme/events';"
},
{
"path": "assets/global.d.ts",
"chars": 1685,
"preview": "export {};\n\ndeclare global {\n interface Shopify {\n country: string;\n currency: {\n active: string;\n rate"
},
{
"path": "assets/header-drawer.js",
"chars": 5469,
"preview": "import { Component } from '@theme/component';\nimport { trapFocus, removeTrapFocus } from '@theme/focus';\nimport { onAnim"
},
{
"path": "assets/header-menu.js",
"chars": 9538,
"preview": "import { Component } from '@theme/component';\nimport { debounce, onDocumentLoaded, setHeaderMenuStyle } from '@theme/uti"
},
{
"path": "assets/header.js",
"chars": 8632,
"preview": "import { Component } from '@theme/component';\nimport { onDocumentLoaded, changeMetaThemeColor, setHeaderMenuStyle } from"
},
{
"path": "assets/jsconfig.json",
"chars": 271,
"preview": "{\n \"compilerOptions\": {\n \"baseUrl\": \"./\",\n \"checkJs\": true,\n \"target\": \"ES2020\",\n \"noImplicitAny\": true,\n "
},
{
"path": "assets/jumbo-text.js",
"chars": 6555,
"preview": "import { ResizeNotifier, prefersReducedMotion, yieldToMainThread } from '@theme/utilities';\nimport { Component } from '@"
},
{
"path": "assets/layered-slideshow.js",
"chars": 18566,
"preview": "import { Component } from '@theme/component';\nimport { isMobileBreakpoint, mediaQueryLarge } from '@theme/utilities';\n\n/"
},
{
"path": "assets/local-pickup.js",
"chars": 2571,
"preview": "import { Component } from '@theme/component';\nimport { morph } from '@theme/morph';\nimport { ThemeEvents, VariantUpdateE"
},
{
"path": "assets/localization.js",
"chars": 17545,
"preview": "import { Component } from '@theme/component';\nimport { isClickedOutside, normalizeString, onAnimationEnd } from '@theme/"
},
{
"path": "assets/marquee.js",
"chars": 7617,
"preview": "import { Component } from '@theme/component';\nimport { debounce } from '@theme/utilities';\n\nconst ANIMATION_OPTIONS = {\n"
},
{
"path": "assets/media-gallery.js",
"chars": 2618,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents, VariantUpdateEvent, ZoomMediaSelectedEvent } from '@"
},
{
"path": "assets/media.js",
"chars": 6766,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents, MediaStartedPlayingEvent } from '@theme/events';\nimp"
},
{
"path": "assets/money-formatting.js",
"chars": 6667,
"preview": "/**\n * Money formatting utilities to replicate Shopify's `money` liquid filter client-side.\n * Using server-side output "
},
{
"path": "assets/morph.js",
"chars": 19581,
"preview": "import { Component } from '@theme/component';\n\n/**\n * @typedef {Object} Options\n * @property {boolean} [childrenOnly] - "
},
{
"path": "assets/overflow-list.css",
"chars": 1249,
"preview": "[part='list'] {\n display: flex;\n flex-wrap: nowrap;\n align-items: center;\n justify-content: var(--overflow-list-alig"
},
{
"path": "assets/overflow-list.js",
"chars": 11495,
"preview": "import { ResizeNotifier } from '@theme/utilities';\nimport { DeclarativeShadowElement } from '@theme/component';\n\n/**\n * "
},
{
"path": "assets/paginated-list-aspect-ratio.js",
"chars": 5419,
"preview": "/**\n * A helper class to keep the set aspect ratio in a card gallery element in the theme editor.\n * This applies the as"
},
{
"path": "assets/paginated-list.js",
"chars": 10146,
"preview": "import { Component } from '@theme/component';\nimport { sectionRenderer } from '@theme/section-renderer';\nimport { reques"
},
{
"path": "assets/performance.js",
"chars": 1588,
"preview": "class ThemePerformance {\n /**\n * @param {string} metricPrefix\n */\n constructor(metricPrefix) {\n this.metricPref"
},
{
"path": "assets/popover-polyfill.js",
"chars": 25893,
"preview": "// src/events.ts\n// @ts-nocheck\n\n/**\n * @fileoverview\n * - Polyfill for the popover attribute, which is not supported in"
},
{
"path": "assets/predictive-search.js",
"chars": 13596,
"preview": "import { Component } from '@theme/component';\nimport { debounce, onAnimationEnd, prefersReducedMotion } from '@theme/uti"
},
{
"path": "assets/price-per-item.js",
"chars": 4186,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents } from '@theme/events';\n\n/**\n * Displays dynamic per-"
},
{
"path": "assets/product-card.js",
"chars": 20710,
"preview": "import { OverflowList } from '@theme/overflow-list';\nimport VariantPicker from '@theme/variant-picker';\nimport { Compone"
},
{
"path": "assets/product-custom-property.js",
"chars": 1043,
"preview": "// assets/product-custom-property.js\nimport { Component } from '@theme/component';\n\n/**\n * @typedef {object} ProductCust"
},
{
"path": "assets/product-form.js",
"chars": 27757,
"preview": "import { Component } from '@theme/component';\nimport { fetchConfig, preloadImage, onAnimationEnd, yieldToMainThread } fr"
},
{
"path": "assets/product-hotspot.js",
"chars": 10580,
"preview": "import { Component } from '@theme/component';\nimport { QuickAddComponent } from '@theme/quick-add';\nimport { isClickedOu"
},
{
"path": "assets/product-inventory.js",
"chars": 1308,
"preview": "import { ThemeEvents, VariantUpdateEvent } from '@theme/events';\nimport { morph } from '@theme/morph';\nimport { Componen"
},
{
"path": "assets/product-price.js",
"chars": 2616,
"preview": "import { ThemeEvents, VariantUpdateEvent } from '@theme/events';\nimport { Component } from '@theme/component';\n\n/**\n * @"
},
{
"path": "assets/product-recommendations.js",
"chars": 5035,
"preview": "import { Component } from '@theme/component';\n\nclass ProductRecommendations extends Component {\n /**\n * The observer "
},
{
"path": "assets/product-sku.js",
"chars": 2247,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents, VariantUpdateEvent } from '@theme/events';\n\n/**\n * A"
},
{
"path": "assets/product-title-truncation.js",
"chars": 2311,
"preview": "import { Component } from '@theme/component';\n\n/** @typedef {typeof globalThis} Window */\n\n/**\n * A component that handl"
},
{
"path": "assets/qr-code-generator.js",
"chars": 45556,
"preview": "/* eslint-disable no-redeclare */\n\n/**\n * @fileoverview\n * - Using the 'QRCode for Javascript library'\n * - Fixed datase"
},
{
"path": "assets/qr-code-image.js",
"chars": 1032,
"preview": "import { QRCode } from '@theme/qr-code-generator';\nimport { Component } from '@theme/component';\n/**\n * A custom element"
},
{
"path": "assets/quick-add.js",
"chars": 11778,
"preview": "import { morph } from '@theme/morph';\nimport { Component } from '@theme/component';\nimport { CartUpdateEvent, ThemeEvent"
},
{
"path": "assets/quick-order-list.js",
"chars": 16356,
"preview": "import { Component } from '@theme/component';\nimport { CartAddEvent, QuantitySelectorUpdateEvent, ThemeEvents } from '@t"
},
{
"path": "assets/recently-viewed-products.js",
"chars": 1154,
"preview": "/**\n * Updates the recently viewed products in localStorage.\n */\nexport class RecentlyViewed {\n /** @static @constant {"
},
{
"path": "assets/results-list.js",
"chars": 1968,
"preview": "import { mediaQueryLarge, requestIdleCallback, startViewTransition } from '@theme/utilities';\nimport PaginatedList from "
},
{
"path": "assets/rte-formatter.js",
"chars": 740,
"preview": "import { Component } from '@theme/component';\n\n/**\n * A custom element that formats rte content for easier styling\n */\nc"
},
{
"path": "assets/scrolling.js",
"chars": 12359,
"preview": "import { debounce, throttle, prefersReducedMotion } from '@theme/utilities';\n\n/**\n * Timeout duration (in milliseconds) "
},
{
"path": "assets/search-page-input.js",
"chars": 1300,
"preview": "import { Component } from '@theme/component';\nimport { debounce } from '@theme/utilities';\n\n/**\n * A custom element that"
},
{
"path": "assets/section-hydration.js",
"chars": 1289,
"preview": "import { buildSectionSelector, normalizeSectionId, sectionRenderer } from '@theme/section-renderer';\nimport { requestIdl"
},
{
"path": "assets/section-renderer.js",
"chars": 5589,
"preview": "import { morph, MORPH_OPTIONS } from '@theme/morph';\n\n/**\n * A class to re-render sections using the Section Rendering A"
},
{
"path": "assets/show-more.js",
"chars": 3964,
"preview": "import { Component } from '@theme/component';\nimport { isMobileBreakpoint } from '@theme/utilities';\n\n/**\n * @typedef {O"
},
{
"path": "assets/slideshow.js",
"chars": 28221,
"preview": "import { Component } from '@theme/component';\nimport {\n center,\n closest,\n clamp,\n mediaQueryLarge,\n prefersReduced"
},
{
"path": "assets/sticky-add-to-cart.js",
"chars": 12214,
"preview": "import { Component } from '@theme/component';\nimport { ThemeEvents, QuantitySelectorUpdateEvent } from '@theme/events';\n"
},
{
"path": "assets/template-giftcard.css",
"chars": 3115,
"preview": ".gift-card {\n --buttons-max-width: min(16rem, 100%);\n --gift-card-image-max-height: 35rem;\n --gift-card-image-max-wid"
},
{
"path": "assets/theme-editor.js",
"chars": 12745,
"preview": "// Theme editor specific logic\nimport { updateAllHeaderCustomProperties } from '@theme/utilities';\n\n/** @type {{ activeS"
},
{
"path": "assets/utilities.js",
"chars": 26477,
"preview": "/**\n * Request an idle callback or fallback to setTimeout\n * @returns {function} The requestIdleCallback function\n */\nex"
},
{
"path": "assets/variant-picker.js",
"chars": 16974,
"preview": "import { Component } from '@theme/component';\nimport { VariantSelectedEvent, VariantUpdateEvent } from '@theme/events';\n"
},
{
"path": "assets/video-background.js",
"chars": 849,
"preview": "import { Component } from '@theme/component';\n\n/**\n * A custom element that renders a video background.\n *\n * @typedef {"
},
{
"path": "assets/view-transitions.js",
"chars": 3961,
"preview": "(function () {\n const viewTransitionRenderBlocker = document.getElementById('view-transition-render-blocker');\n // Rem"
},
{
"path": "assets/volume-pricing-info.js",
"chars": 2301,
"preview": "import { Component } from '@theme/component';\n\n/**\n * Displays volume pricing information in a popover.\n * Shows quantit"
},
{
"path": "assets/volume-pricing.js",
"chars": 508,
"preview": "import { Component } from '@theme/component';\n\n/**\n * Displays volume pricing table with expandable rows.\n * Shows prici"
},
{
"path": "assets/zoom-dialog.js",
"chars": 9371,
"preview": "import { Component } from '@theme/component';\nimport {\n supportsViewTransitions,\n startViewTransition,\n onAnimationEn"
},
{
"path": "blocks/_accordion-row.liquid",
"chars": 7904,
"preview": "{% assign block_settings = block.settings %}\n\n<accordion-custom\n {% if block_settings.open_by_default %}\n open-by-de"
},
{
"path": "blocks/_announcement.liquid",
"chars": 7076,
"preview": "{%- assign block_settings = block.settings -%}\n{%- assign plain_text = block_settings.text | strip_newlines | strip_html"
},
{
"path": "blocks/_blog-post-card.liquid",
"chars": 2590,
"preview": "{%- assign block_settings = block.settings -%}\n\n<div\n class=\"blog-post-card\"\n style=\"--text-align: {{ block_settings.a"
},
{
"path": "blocks/_blog-post-content.liquid",
"chars": 379,
"preview": "<div class=\"blog-post-content rte\">\n <rte-formatter>\n {{ article.content }}\n </rte-formatter>\n</div>\n\n{% stylesheet"
},
{
"path": "blocks/_blog-post-description.liquid",
"chars": 8337,
"preview": "{%- doc -%}\n Renders the blog post description block.\n\n @param {object} article - The article object\n @param {boolean"
},
{
"path": "blocks/_blog-post-featured-image.liquid",
"chars": 6574,
"preview": "{%- doc -%}\n Renders the blog post featured image block.\n{%- enddoc -%}\n\n{% liquid\n assign block_settings = block.sett"
},
{
"path": "blocks/_blog-post-image.liquid",
"chars": 3142,
"preview": "{%- doc -%}\n Renders the blog post image block.\n\n @param {object} article - The article object\n{%- enddoc -%}\n\n{%- ass"
},
{
"path": "blocks/_blog-post-info-text.liquid",
"chars": 3910,
"preview": "{%- doc -%}\n Renders the blog post info text block.\n\n @param {object} [article] - The article object\n @param {string}"
},
{
"path": "blocks/_card.liquid",
"chars": 13032,
"preview": "{% assign block_settings = block.settings %}\n\n{% capture children %}\n {% content_for 'blocks' %}\n{% endcapture %}\n\n<div"
},
{
"path": "blocks/_carousel-content.liquid",
"chars": 6136,
"preview": "{% liquid\n assign slide_count = block.blocks.size\n assign icons_style = section.settings.icons_style\n\n assign color_s"
},
{
"path": "blocks/_cart-products.liquid",
"chars": 2023,
"preview": "{% render 'cart-products' %}\n\n{% stylesheet %}\n .cart-page__title + .cart-page__items {\n margin-block-start: var(--m"
},
{
"path": "blocks/_cart-summary.liquid",
"chars": 5960,
"preview": "{% assign block_settings = block.settings %}\n{%- capture cart_summary_inner_class -%}\n cart-summary__inner\n {% if bloc"
},
{
"path": "blocks/_cart-title.liquid",
"chars": 4013,
"preview": "{% doc %}\n Cart Title\n\n Renders the cart title or empty cart state.\n\n @param {boolean} [force_empty] - Force render e"
},
{
"path": "blocks/_collection-card-image.liquid",
"chars": 3822,
"preview": "{%- doc -%}\n Display an image of a collection inside a collection card.\n Intended for collection-card.liquid block.\n\n "
},
{
"path": "blocks/_collection-card.liquid",
"chars": 3909,
"preview": "{% assign collection = closest.collection %}\n\n{% capture card_image %}\n {% content_for 'block',\n type: '_collection-"
},
{
"path": "blocks/_collection-image.liquid",
"chars": 4029,
"preview": "{% assign block_settings = block.settings %}\n\n{% if block_settings.image_ratio == 'custom' %}\n {% assign image_width = "
},
{
"path": "blocks/_collection-info.liquid",
"chars": 3719,
"preview": "{% assign block_settings = block.settings %}\n<div\n class=\"collection-info collection-info--{{ block_settings.placement "
},
{
"path": "blocks/_collection-link.liquid",
"chars": 4398,
"preview": "{%- doc -%}\n Renders a collection link block.\n\n @param {number} index\n @param {boolean} [current]\n @param {boolean} "
},
{
"path": "blocks/_content-without-appearance.liquid",
"chars": 2059,
"preview": "{% capture children %}\n {% content_for 'blocks' %}\n{% endcapture %}\n\n{% render 'group',\n class: 'media-with-content__c"
},
{
"path": "blocks/_content.liquid",
"chars": 3259,
"preview": "{% capture children %}\n {% content_for 'blocks' %}\n{% endcapture %}\n\n{% render 'group', children: children, settings: b"
},
{
"path": "blocks/_divider.liquid",
"chars": 1528,
"preview": "{% render 'divider', id: block.id, settings: block.settings, attributes: true %}\n\n{% schema %}\n{\n \"name\": \"t:names.divi"
},
{
"path": "blocks/_featured-blog-posts-card.liquid",
"chars": 6953,
"preview": "{% assign block_settings = block.settings %}\n\n{% liquid\n assign onboarding = false\n\n if article == blank\n assign on"
},
{
"path": "blocks/_featured-blog-posts-image.liquid",
"chars": 2516,
"preview": "{%- doc -%}\n Renders the blog article image block for use within featured blog posts cards.\n\n @param {object} image - "
},
{
"path": "blocks/_featured-blog-posts-title.liquid",
"chars": 9043,
"preview": "{%- doc -%}\n Featured Blog Posts Title Block\n\n Renders a blog title with text styling options.\n Uses the blog title f"
},
{
"path": "blocks/_featured-product-gallery.liquid",
"chars": 1828,
"preview": "{% liquid\n assign product = closest.product\n%}\n\n{% capture children %}\n {% unless product == blank %}\n {% if settin"
},
{
"path": "blocks/_featured-product-information-carousel.liquid",
"chars": 6073,
"preview": "{% render 'product-media-gallery-content',\n media_presentation: 'carousel',\n block_settings: block.settings,\n block_i"
},
{
"path": "blocks/_featured-product-price.liquid",
"chars": 2660,
"preview": "{% liquid\n assign product = closest.product\n assign block_settings = block.settings\n\n if settings.currency_code_enabl"
},
{
"path": "blocks/_featured-product.liquid",
"chars": 1351,
"preview": "{%- liquid\n assign product_has_swatches = false\n for product_option in closest.product.options_with_values\n assign "
},
{
"path": "blocks/_footer-social-icons.liquid",
"chars": 528,
"preview": "<div\n class=\"social-icons__wrapper\"\n {{ block.shopify_attributes }}\n>\n {% content_for 'blocks' %}\n</div>\n\n{% styleshe"
},
{
"path": "blocks/_header-logo.liquid",
"chars": 6175,
"preview": "{% liquid\n assign block_settings = block.settings\n assign use_inverse_logo = false\n\n if section.settings.enable_trans"
},
{
"path": "blocks/_header-menu.liquid",
"chars": 25168,
"preview": "{%- doc -%}\n Renders a header menu block.\n\n @param {string} [variant] - What version of the menu to render. Defaults t"
},
{
"path": "blocks/_heading.liquid",
"chars": 7345,
"preview": "{%- doc -%}\n Renders a heading block.\n\n @param {string} text\n{%- enddoc -%}\n\n{% render 'text', width: '100%', block: b"
},
{
"path": "blocks/_hotspot-product.liquid",
"chars": 3434,
"preview": "{% liquid\n assign hotspot_product = closest.product\n\n assign placeholder_product_title = 'placeholders.product_title' "
},
{
"path": "blocks/_image.liquid",
"chars": 4184,
"preview": "{%- doc -%}\n Renders an image block.\n\n @param {string} [loading] - The html loading attribute\n @param {string} [place"
},
{
"path": "blocks/_inline-collection-title.liquid",
"chars": 3940,
"preview": "{%- doc -%}\n @param {string} [suffix] - The suffix to add to the collection title\n{%- enddoc -%}\n\n{%- assign block_sett"
},
{
"path": "blocks/_inline-text.liquid",
"chars": 3683,
"preview": "{%- doc -%}\n Renders an inline text block.\n\n @param {string} suffix\n{%- enddoc -%}\n\n{% assign block_settings = block.s"
},
{
"path": "blocks/_layered-slide.liquid",
"chars": 10647,
"preview": "{%- assign block_index = section.blocks | find_index: 'id', block.id -%}\n\n{%- liquid\n assign preview_image = block.sett"
},
{
"path": "blocks/_marquee.liquid",
"chars": 4799,
"preview": "<script\n src=\"{{ 'marquee.js' | asset_url }}\"\n type=\"module\"\n fetchpriority=\"low\"\n></script>\n\n{% assign block_setting"
},
{
"path": "blocks/_media-without-appearance.liquid",
"chars": 2363,
"preview": "{% liquid\n assign unset_image_tag = false\n if block.settings.image_position == 'contain'\n assign unset_image_tag = "
},
{
"path": "blocks/_media.liquid",
"chars": 2872,
"preview": "{% render 'media', section_id: section.id %}\n\n{% schema %}\n{\n \"name\": \"t:names.media\",\n \"tag\": null,\n \"settings\": [\n "
},
{
"path": "blocks/_product-card-gallery.liquid",
"chars": 5910,
"preview": "{% assign product = closest.product %}\n\n{% capture children %}\n {% unless product == blank %}\n <div\n class=\"pro"
},
{
"path": "blocks/_product-card-group.liquid",
"chars": 11578,
"preview": "{% capture children %}\n {% content_for 'blocks' %}\n{% endcapture %}\n\n{% render 'group', children: children, settings: b"
},
{
"path": "blocks/_product-card.liquid",
"chars": 3392,
"preview": "{% liquid\n assign product = closest.product\n-%}\n\n{% capture children %}\n {% content_for 'blocks', closest.product: pro"
},
{
"path": "blocks/_product-details.liquid",
"chars": 18029,
"preview": "{%- assign block_settings = block.settings -%}\n<div\n id=\"ProductInformation-{{ section.id }}\"\n class=\"\n product-det"
},
{
"path": "blocks/_product-list-button.liquid",
"chars": 2700,
"preview": "{% assign button_collection = closest.collection %}\n{% assign button_url = button_collection.url %}\n\n{% if button_collec"
},
{
"path": "blocks/_product-list-content.liquid",
"chars": 10038,
"preview": "{%- capture children -%}\n {%- content_for 'blocks' -%}\n{%- endcapture -%}\n\n{%- render 'group', children: children, sett"
},
{
"path": "blocks/_product-list-text.liquid",
"chars": 8695,
"preview": "{% assign placeholder = 'content.featured_products' | t %}\n{% assign placeholder = '<h3>' | append: placeholder | append"
},
{
"path": "blocks/_product-media-gallery.liquid",
"chars": 7779,
"preview": "{% render 'product-media-gallery-content',\n media_presentation: block.settings.media_presentation,\n block_settings: bl"
},
{
"path": "blocks/_search-input.liquid",
"chars": 6542,
"preview": "{%- assign block_settings = block.settings -%}\n\n<script\n src=\"{{ 'search-page-input.js' | asset_url }}\"\n defer\n type="
},
{
"path": "blocks/_slide.liquid",
"chars": 13343,
"preview": "{% assign block_settings = block.settings %}\n{%- assign block_index = section.blocks | find_index: 'id', block.id -%}\n{%"
},
{
"path": "blocks/_social-link.liquid",
"chars": 2695,
"preview": "{% liquid\n assign block_settings = block.settings\n\n if block_settings.link != blank\n # Extract domain from URL\n "
},
{
"path": "blocks/accelerated-checkout.liquid",
"chars": 1642,
"preview": "{%- doc -%}\n This block is used to display the accelerated checkout button.\n Intended for product-form.liquid block.\n\n"
},
{
"path": "blocks/accordion.liquid",
"chars": 7966,
"preview": "{% assign block_settings = block.settings %}\n<div\n class=\"accordion accordion--{{ block.id }} accordion--{{ block_setti"
},
{
"path": "blocks/add-to-cart.liquid",
"chars": 1226,
"preview": "{%- doc -%}\n This block is used to display the add to cart button.\n Intended for product-form.liquid block.\n\n @param "
},
{
"path": "blocks/button.liquid",
"chars": 2411,
"preview": "{% render 'button', link: block.settings.link %}\n\n{% schema %}\n{\n \"name\": \"t:names.button\",\n \"tag\": null,\n \"settings\""
},
{
"path": "blocks/buy-buttons.liquid",
"chars": 23496,
"preview": "{% liquid\n assign block_settings = block.settings\n assign product = closest.product\n if request.visual_preview_mode a"
},
{
"path": "blocks/collection-card.liquid",
"chars": 4424,
"preview": "{% assign collection = block.settings.collection %}\n\n{% capture card_image %}\n {% content_for 'block',\n type: '_coll"
},
{
"path": "blocks/collection-title.liquid",
"chars": 8874,
"preview": "{% if closest.collection == blank %}\n {% assign text = 'placeholders.collection_title' | t %}\n {% assign collection_ti"
},
{
"path": "blocks/comparison-slider.liquid",
"chars": 19938,
"preview": "<script\n src=\"{{ 'comparison-slider.js' | asset_url }}\"\n type=\"module\"\n fetchpriority=\"low\"\n></script>\n\n{% liquid\n a"
},
{
"path": "blocks/contact-form-submit-button.liquid",
"chars": 2270,
"preview": "{%- assign block_settings = block.settings -%}\n<button\n type=\"submit\"\n class=\"button submit-button size-style {{ block"
},
{
"path": "blocks/contact-form.liquid",
"chars": 7202,
"preview": "{% capture submit_button %}\n {% content_for 'block', type: 'contact-form-submit-button', id: 'submit-button' %}\n{% endc"
},
{
"path": "blocks/custom-liquid.liquid",
"chars": 410,
"preview": "<div>\n {{ block.settings.custom_liquid }}\n</div>\n\n{% schema %}\n{\n \"name\": \"t:names.custom_liquid\",\n \"tag\": null,\n \"s"
},
{
"path": "blocks/email-signup.liquid",
"chars": 18011,
"preview": "{% assign block_settings = block.settings %}\n<div\n class=\"email-signup-block size-style spacing-style\"\n style=\"{% rend"
},
{
"path": "blocks/featured-collection.liquid",
"chars": 432,
"preview": "<div\n class=\"featured-collection-block\"\n {{ block.shopify_attributes }}\n>\n {% content_for 'blocks' %}\n</div>\n\n{% sche"
},
{
"path": "blocks/filters.liquid",
"chars": 42885,
"preview": "{%- doc -%}\n Renders the facet filtering component\n\n @param {object} results - The search results object\n @param {num"
},
{
"path": "blocks/follow-on-shop.liquid",
"chars": 1418,
"preview": "{%- if shop.features.follow_on_shop? -%}\n <div\n class=\"spacing-style\"\n style=\"{% render 'spacing-padding', settin"
},
{
"path": "blocks/footer-copyright.liquid",
"chars": 1847,
"preview": "{% assign block_settings = block.settings %}\n<div\n class=\"\n footer-utilities__group-copyright\n custom-typography\n"
},
{
"path": "blocks/footer-policy-list.liquid",
"chars": 4984,
"preview": "{% if shop.policies.size > 0 %}\n <anchored-popover-component\n data-hover-triggered=\"true\"\n >\n <button\n clas"
},
{
"path": "blocks/group.liquid",
"chars": 11438,
"preview": "{%- capture children %}\n {% content_for 'blocks' %}\n{% endcapture %}\n\n{% render 'group', children: children, settings: "
},
{
"path": "blocks/icon.liquid",
"chars": 7116,
"preview": "{% assign block_settings = block.settings %}\n{% assign icon_block_class = 'icon-block__media icon-block-' | append: bloc"
},
{
"path": "blocks/image.liquid",
"chars": 7205,
"preview": "{% liquid\n assign block_settings = block.settings\n assign ratio = 1\n\n case block_settings.image_ratio\n when 'lands"
},
{
"path": "blocks/jumbo-text.liquid",
"chars": 2829,
"preview": "{% render 'jumbo-text' %}\n\n{% schema %}\n{\n \"name\": \"t:names.jumbo_text\",\n \"tag\": null,\n \"settings\": [\n {\n \"id"
}
]
// ... and 225 more files (download for full content)
About this extraction
This page contains the full source code of the Shopify/horizon GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 425 files (4.5 MB), approximately 1.2M tokens, and a symbol index with 833 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.