Skip to content

WIP(5514): Short-term Slick carousel accessibility patch (theme JS)#5528

Draft
accesswatch wants to merge 1 commit intomainfrom
fix/5514-carousel-a11y-patch
Draft

WIP(5514): Short-term Slick carousel accessibility patch (theme JS)#5528
accesswatch wants to merge 1 commit intomainfrom
fix/5514-carousel-a11y-patch

Conversation

@accesswatch
Copy link
Copy Markdown
Contributor

@accesswatch accesswatch commented Apr 23, 2026

Addresses #5514 (short-term mitigation) — Why: the Slick carousel library generates runtime ARIA markup that triggers several critical axe-core violations (invalid roles/attributes, focusable elements hidden with aria-hidden, missing accessible names).

What this change does:

  • Adds an optional theme JS runtime patch that applies pragmatic corrections when Slick initializes:
    • Adds an accessible aria-label to the slides track when missing.
    • Converts .slick-dots to role="list" and removes invalid ARIA attributes from dot <li> items.
    • Ensures dot buttons have aria-label text (e.g., "Go to slide N").
    • Removes problematic role/tabindex from slide wrappers and prevents focusable children inside aria-hidden slides.

Impact:

  • Mitigates the most critical ARIA violations reported by automated scans and improves keyboard/focus behavior.
  • This is an intermediate, opt-in mitigation; the recommended long-term solution is to migrate away from Slick to an accessibility-first carousel library.

How to test:

  1. Enable the theme library az-slick-a11y-patch on a site using az_carousel (or ensure the theme loads the patch JS).
  2. Initialize a carousel with dot navigation and run axe-core or Accessibility Insights; confirm the previously reported violations are resolved.
  3. Test keyboard navigation and screen-reader announcements for the carousel and dot controls.
  4. Validate there are no regressions in autoplay/slide behavior.

Notes:

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an optional AZ Barrio theme JS “runtime patch” to mitigate Slick carousel accessibility issues reported in #5514 by modifying Slick’s generated ARIA/focus markup during initialization.

Changes:

  • Introduces slick-a11y-patch.js to adjust ARIA roles/labels and focus handling for Slick carousels at runtime.
  • Adds a new theme library entry intended to ship/enable the patch JS.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
themes/custom/az_barrio/js/slick-a11y-patch.js Runtime DOM patching on Slick init/reInit to reduce axe-core ARIA violations and adjust focusability.
themes/custom/az_barrio/az_barrio.libraries.yml Adds a theme library to include the new patch JS (currently duplicated).

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +5 to +37
(function ($, Drupal) {
'use strict';
$(document).once('slick-a11y-patch').each(function () {
// When slick initializes, apply corrections
$(document).on('init reInit', '.slick-slider', function (event, slick) {
var $slider = $(this);
// ensure track has accessible name
var $track = $slider.find('.slick-track');
if ($track.length && !$track.attr('aria-label')) {
$track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides');
}
// fix dots role
var $dots = $slider.find('.slick-dots');
if ($dots.length) {
$dots.attr('role', 'list');
$dots.find('li').each(function (i) {
$(this).removeAttr('role aria-selected aria-controls');
// ensure internal button has accessible label
var $btn = $(this).find('button');
if ($btn.length && !$btn.attr('aria-label')) {
$btn.attr('aria-label', 'Go to slide ' + (i+1));
}
});
}
// hide focusable children of aria-hidden slides
$slider.find('.slick-slide[aria-hidden="true"]').each(function () {
$(this).find('a, button, input, select, textarea').attr('tabindex', '-1');
});
// remove problematic role on slide wrapper
$slider.find('.slick-slide').removeAttr('role tabindex');
});
});
})(jQuery, Drupal);
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$(document).once(...) relies on the old jQuery Once plugin, which isn’t available by default in Drupal core 11 (core provides the once() utility via core/once). As written, this will likely throw $(...).once is not a function at runtime. Update this to use once() (and/or a Drupal behavior) and ensure the library depends on core/once.

Suggested change
(function ($, Drupal) {
'use strict';
$(document).once('slick-a11y-patch').each(function () {
// When slick initializes, apply corrections
$(document).on('init reInit', '.slick-slider', function (event, slick) {
var $slider = $(this);
// ensure track has accessible name
var $track = $slider.find('.slick-track');
if ($track.length && !$track.attr('aria-label')) {
$track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides');
}
// fix dots role
var $dots = $slider.find('.slick-dots');
if ($dots.length) {
$dots.attr('role', 'list');
$dots.find('li').each(function (i) {
$(this).removeAttr('role aria-selected aria-controls');
// ensure internal button has accessible label
var $btn = $(this).find('button');
if ($btn.length && !$btn.attr('aria-label')) {
$btn.attr('aria-label', 'Go to slide ' + (i+1));
}
});
}
// hide focusable children of aria-hidden slides
$slider.find('.slick-slide[aria-hidden="true"]').each(function () {
$(this).find('a, button, input, select, textarea').attr('tabindex', '-1');
});
// remove problematic role on slide wrapper
$slider.find('.slick-slide').removeAttr('role tabindex');
});
});
})(jQuery, Drupal);
(function ($, Drupal, once) {
'use strict';
Drupal.behaviors.slickA11yPatch = {
attach: function (context) {
once('slick-a11y-patch', document).forEach(function () {
// When slick initializes, apply corrections
$(document).on('init reInit', '.slick-slider', function (event, slick) {
var $slider = $(this);
// ensure track has accessible name
var $track = $slider.find('.slick-track');
if ($track.length && !$track.attr('aria-label')) {
$track.attr('aria-label', $slider.data('a11y-label') || 'Carousel slides');
}
// fix dots role
var $dots = $slider.find('.slick-dots');
if ($dots.length) {
$dots.attr('role', 'list');
$dots.find('li').each(function (i) {
$(this).removeAttr('role aria-selected aria-controls');
// ensure internal button has accessible label
var $btn = $(this).find('button');
if ($btn.length && !$btn.attr('aria-label')) {
$btn.attr('aria-label', 'Go to slide ' + (i + 1));
}
});
}
// hide focusable children of aria-hidden slides
$slider.find('.slick-slide[aria-hidden="true"]').each(function () {
$(this).find('a, button, input, select, textarea').attr('tabindex', '-1');
});
// remove problematic role on slide wrapper
$slider.find('.slick-slide').removeAttr('role tabindex');
});
});
}
};
})(jQuery, Drupal, once);

Copilot uses AI. Check for mistakes.
Comment on lines +29 to +32
// hide focusable children of aria-hidden slides
$slider.find('.slick-slide[aria-hidden="true"]').each(function () {
$(this).find('a, button, input, select, textarea').attr('tabindex', '-1');
});
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The patch sets tabindex="-1" on focusable elements in aria-hidden="true" slides, but it never restores the original tabindex when a slide becomes visible again. After the first slide change, previously-hidden slides can remain unfocusable even when active. Consider also handling Slick’s slide-change events (e.g., afterChange) and restoring tabindex (ideally only removing/undoing tabindex values that this patch added).

Copilot uses AI. Check for mistakes.
Comment on lines +16 to +27
// fix dots role
var $dots = $slider.find('.slick-dots');
if ($dots.length) {
$dots.attr('role', 'list');
$dots.find('li').each(function (i) {
$(this).removeAttr('role aria-selected aria-controls');
// ensure internal button has accessible label
var $btn = $(this).find('button');
if ($btn.length && !$btn.attr('aria-label')) {
$btn.attr('aria-label', 'Go to slide ' + (i+1));
}
});
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dot-nav li elements commonly have aria-hidden set by Slick (and contain focusable <button> children), which is one of the core aria-hidden-focus violations described in #5514. This patch removes role/aria-selected/aria-controls from the li, but it leaves aria-hidden in place and doesn’t adjust the nested button focusability, so the aria-hidden-focus violation may still remain for dots. Consider removing aria-hidden from dot li elements (or otherwise ensuring their buttons are not focusable when hidden).

Copilot uses AI. Check for mistakes.
Comment on lines +86 to +92

az-slick-a11y-patch:
js:
js/slick-a11y-patch.js: {}
dependencies:
- core/jquery
- core/drupal
Copy link

Copilot AI Apr 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

az-slick-a11y-patch is defined twice in this YAML. Duplicate keys are invalid YAML and can lead to parsing errors or unexpected overrides (only the last definition may win). Remove the duplicate entry, and (since the JS uses once() semantics) add the appropriate dependency (likely core/once) to the remaining library definition.

Suggested change
az-slick-a11y-patch:
js:
js/slick-a11y-patch.js: {}
dependencies:
- core/jquery
- core/drupal
- core/once

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants