diff --git a/lib/core/utils/index.js b/lib/core/utils/index.js
index 6600f497b4..8f919d1546 100644
--- a/lib/core/utils/index.js
+++ b/lib/core/utils/index.js
@@ -77,6 +77,7 @@ export { default as select } from './select';
export { default as sendCommandToFrame } from './send-command-to-frame';
export { default as setScrollState } from './set-scroll-state';
export { default as shadowSelect } from './shadow-select';
+export { default as shadowSelectAll } from './shadow-select-all';
export { default as toArray } from './to-array';
export { default as tokenList } from './token-list';
export { default as uniqueArray } from './unique-array';
diff --git a/lib/core/utils/shadow-select-all.js b/lib/core/utils/shadow-select-all.js
new file mode 100644
index 0000000000..2f311a40e6
--- /dev/null
+++ b/lib/core/utils/shadow-select-all.js
@@ -0,0 +1,31 @@
+/**
+ * Find elements to match a selector.
+ * Use an array of selectors to reach into shadow DOM trees
+ *
+ * @param {string|string[]} selector String or array of strings with a CSS selector
+ * @param {Document} doc Optional document node
+ * @returns {Node[]}
+ */
+export default function shadowSelectAll(selectors, doc = document) {
+ // Spread to avoid mutating the input
+ const selectorArr = Array.isArray(selectors) ? [...selectors] : [selectors];
+ if (selectors.length === 0) {
+ return [];
+ }
+ return selectAllRecursive(selectorArr, doc);
+}
+
+/* Find elements in shadow or light DOM trees, using an array of selectors */
+function selectAllRecursive([selectorStr, ...restSelector], doc) {
+ const elms = doc.querySelectorAll(selectorStr);
+ if (restSelector.length === 0) {
+ return Array.from(elms);
+ }
+ const selected = [];
+ for (const elm of elms) {
+ if (elm?.shadowRoot) {
+ selected.push(...selectAllRecursive(restSelector, elm.shadowRoot));
+ }
+ }
+ return selected;
+}
diff --git a/test/core/utils/shadow-select-all.js b/test/core/utils/shadow-select-all.js
new file mode 100644
index 0000000000..225f6685b0
--- /dev/null
+++ b/test/core/utils/shadow-select-all.js
@@ -0,0 +1,93 @@
+describe('utils.shadowSelectAll', () => {
+ const shadowSelectAll = axe.utils.shadowSelectAll;
+ const fixture = document.querySelector('#fixture');
+ const mapNodeName = elms => elms.map(elm => elm.nodeName.toLowerCase());
+
+ it('throws when not passed a string or array', () => {
+ assert.throws(() => {
+ shadowSelectAll(123);
+ });
+ });
+
+ it('throws when passed an array with non-string values', () => {
+ assert.throws(() => {
+ shadowSelectAll([123]);
+ });
+ });
+
+ describe('given a string', () => {
+ it('returns [] if no node is found', () => {
+ fixture.innerHTML = '';
+ assert.deepEqual(shadowSelectAll('.goodbye'), []);
+ });
+
+ it('returns the each matching element in the document', () => {
+ fixture.innerHTML = `
+
+ `;
+ const nodes = shadowSelectAll('#fixture > .hello');
+ assert.deepEqual(mapNodeName(nodes), ['b', 'i']);
+ });
+ });
+
+ describe('given an array of string', () => {
+ function addShadowTree(host, html) {
+ const root = host.attachShadow({ mode: 'open' });
+ root.innerHTML = html;
+ return root;
+ }
+
+ it('returns [] given an empty array', () => {
+ assert.deepEqual(shadowSelectAll([]), []);
+ });
+
+ it('returns [] if the shadow host does not exist', () => {
+ fixture.innerHTML = '