JS / DOM и события
Список с кнопкой «Показать ещё»: доступная реализация на HTML, CSS и JavaScript
Создаём список меток с показом и скрытием дополнительных пунктов, сохраняя семантику, клавиатуру и работу без JavaScript.
Содержание статьи
Длинный список категорий или технологий можно свернуть до нескольких пунктов и раскрывать по запросу. Для этого понадобятся семантический список, настоящая кнопка и небольшой JavaScript-модуль.
Важный принцип progressive enhancement: без JavaScript пользователь должен видеть все ссылки. Скрипт сам добавит начальное свёрнутое состояние после загрузки.
HTML
<section class="tag-list" data-collapsible-list data-visible-count="5"> <h2 class="tag-list__title">Темы</h2> <ul class="tag-list__items" id="topic-list"> <li><a href="/topics/html">HTML</a></li> <li><a href="/topics/css">CSS</a></li> <li><a href="/topics/javascript">JavaScript</a></li> <li><a href="/topics/php">PHP</a></li> <li><a href="/topics/sql">SQL</a></li> <li><a href="/topics/accessibility">Доступность</a></li> <li><a href="/topics/performance">Производительность</a></li> <li><a href="/topics/security">Безопасность</a></li> </ul> <button class="tag-list__toggle" type="button" aria-expanded="true" aria-controls="topic-list" hidden > Скрыть дополнительные темы </button></section>Список остаётся <ul>, а действие — <button>. aria-controls указывает на управляемую область, aria-expanded отражает текущее состояние. Кнопка изначально скрыта, потому что без JavaScript она ничего не делает.
CSS
.tag-list__items { display: flex; flex-wrap: wrap; gap: 0.75rem; padding: 0; margin: 0; list-style: none;}.tag-list__items a { display: inline-flex; padding: 0.45rem 0.75rem; color: #3b176f; background-color: #f4f0ff; border: 1px solid #d9ccff; border-radius: 999px;}.tag-list__toggle { margin-block-start: 1rem; font: inherit;}Скрывать элементы будет атрибут hidden, которому браузер уже задаёт display: none. Не переопределяйте [hidden] глобальным display, иначе скрытые пункты могут появиться.
JavaScript
const lists = document.querySelectorAll('[data-collapsible-list]');for (const root of lists) { const items = [...root.querySelectorAll('.tag-list__items > li')]; const button = root.querySelector('.tag-list__toggle'); const visibleCount = Number.parseInt(root.dataset.visibleCount ?? '', 10); if (!button || !Number.isInteger(visibleCount) || visibleCount < 1) { continue; } const extraItems = items.slice(visibleCount); if (extraItems.length === 0) { continue; } function render(isExpanded) { for (const item of extraItems) { item.hidden = !isExpanded; } button.setAttribute('aria-expanded', String(isExpanded)); button.textContent = isExpanded ? 'Скрыть дополнительные темы' : `Показать ещё (${extraItems.length})`; } button.hidden = false; button.addEventListener('click', () => { const isExpanded = button.getAttribute('aria-expanded') === 'true'; render(!isExpanded); }); render(false);}Код поддерживает несколько независимых списков. Настройка data-visible-count превращается в число и проверяется до использования. Если элементов недостаточно или разметка неполная, список остаётся полностью видимым.
Почему не нужно менять innerHTML
Список уже существует в документе. Нет причины пересобирать его строкой:
// Не нужно: удаляет существующие узлы и может создать XSS при недоверенных данных.container.innerHTML = items.map((item) => `<li>${item}</li>`).join('');Атрибут hidden меняет только видимость существующих элементов. Ссылки, обработчики и семантика сохраняются.
Если элементы приходят из API, создавайте узлы и записывайте пользовательский текст через textContent:
function createTag(label, url) { const item = document.createElement('li'); const link = document.createElement('a'); link.href = url; link.textContent = label; item.append(link); return item;}URL тоже должен быть проверен по допустимым протоколам и источнику данных.
Фокус после сворачивания
В текущей реализации пользователь сворачивает список кнопкой, поэтому фокус остаётся на видимой кнопке. Если состояние может измениться извне, проверьте, не находится ли фокус внутри скрываемого пункта. Сначала перенесите его на управляющую кнопку, затем скройте область.
Анимация
hidden не анимируется. Для небольшого списка мгновенное изменение обычно лучше сложной анимации высоты. Если добавляете движение, не фиксируйте высоту по количеству пунктов и учитывайте prefers-reduced-motion.
Что проверить
- без JavaScript видны все пункты, а кнопки нет;
- кнопка работает Enter и пробелом;
aria-expandedсовпадает с видимостью;- при пяти или менее элементах кнопка не появляется;
- длинные названия переносятся и не раздвигают страницу;
- несколько списков работают независимо;
- повторное раскрытие возвращает все ссылки.
Что запомнить
Начинайте с полноценного HTML-списка и добавляйте сворачивание как улучшение. Используйте кнопку, hidden, aria-expanded и локальный поиск элементов внутри компонента. Не прячьте важные ссылки от пользователя только потому, что JavaScript не загрузился.