diff --git a/docs/assets/scripts/cheatsheet.js b/docs/assets/scripts/cheatsheet.js
new file mode 100644
index 000000000..3a9cbc929
--- /dev/null
+++ b/docs/assets/scripts/cheatsheet.js
@@ -0,0 +1,76 @@
+let url = new URL(location);
+const pushedURL = false;
+
+const matchers = {
+ default(textContent, query) {
+ return textContent.includes(query);
+ },
+
+ i(textContent, query) {
+ return textContent.toLowerCase().includes(query.toLowerCase());
+ },
+
+ regexp(textContent, query) {
+ query.lastIndex = 0;
+ return query.test(textContent);
+ }
+};
+
+matchers.iregexp = matchers.regexp; // i is baked into the query
+
+function filterByName(value) {
+ const previousFilter = url.searchParams.get('name') || '';
+ url = new URL(location);
+
+ if (value) {
+ const isRegexp = name_search_regexp.checked;
+ const i = !name_search_i.checked;
+ const query = isRegexp ? new RegExp(value, 'gmsv' + (i ? 'i' : '')) : value;
+ const matcherId = (i ? 'i' : '') + (isRegexp ? 'regexp' : '');
+ const matcher = matchers[matcherId] ?? matchers.default;
+
+ for (const th of document.querySelectorAll('table tbody th:first-child')) {
+ const tr = th.parentNode;
+ const matches = matcher(th.textContent, query);
+ tr.toggleAttribute('hidden', !matches);
+ }
+ url.searchParams.set('name', value);
+
+ if (matcherId) {
+ url.searchParams.set('match', matcherId);
+ } else {
+ url.searchParams.delete('match');
+ }
+ } else {
+ for (const tr of document.querySelectorAll('table tbody tr[hidden]')) {
+ tr.removeAttribute('hidden');
+ }
+ url.searchParams.delete('name');
+ url.searchParams.delete('match');
+ }
+
+ if (value !== previousFilter) {
+ history[pushedURL ? 'replaceState' : 'pushState'](null, '', url);
+ }
+
+ // Update heading counts
+ for (const h2 of document.querySelectorAll('h2:has(+ table)')) {
+ const count = h2.querySelector('.count');
+ if (!count) continue;
+ const table = h2.nextElementSibling;
+ const visibleRows = table.querySelectorAll('tbody tr:not([hidden])').length;
+ count.textContent = visibleRows;
+ const outlineLink = document.querySelector(`#outline-standard a[href="#${h2.id}"]`);
+ if (outlineLink) {
+ // Why not just = h2.textContent? To skip the "Jump to heading" link
+ outlineLink.textContent = '';
+ outlineLink.append(...[...h2.childNodes].slice(0, 3).map(n => n.cloneNode(true)));
+ }
+ }
+}
+
+if (name_search.value) {
+ filterByName(name_search.value);
+}
+
+name_search_group.addEventListener('wa-input', e => filterByName(name_search.value));
diff --git a/docs/docs/components/cheatsheet.njk b/docs/docs/components/cheatsheet.njk
index 29f987c89..9e2f9cca3 100644
--- a/docs/docs/components/cheatsheet.njk
+++ b/docs/docs/components/cheatsheet.njk
@@ -43,86 +43,7 @@ table code {
}
}
-
+
{% for type, all in componentsBy -%}
{% set typeTitle = "CSS custom properties" if type == "cssProperty" else ("CSS parts" if type == "cssPart" else (type | title) + "s") %}