Список с кнопкой «Показать ещё»: доступная реализация на HTML, CSS и JavaScript

JS / DOM и события

Список с кнопкой «Показать ещё»: доступная реализация на HTML, CSS и JavaScript

Создаём список меток с показом и скрытием дополнительных пунктов, сохраняя семантику, клавиатуру и работу без JavaScript.

Опубликовано 04.09.2026 5 минут чтения
Содержание статьи

Длинный список категорий или технологий можно свернуть до нескольких пунктов и раскрывать по запросу. Для этого понадобятся семантический список, настоящая кнопка и небольшой JavaScript-модуль.

Важный принцип progressive enhancement: без JavaScript пользователь должен видеть все ссылки. Скрипт сам добавит начальное свёрнутое состояние после загрузки.

HTML

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

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

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

Список уже существует в документе. Нет причины пересобирать его строкой:

JavaScript
// Не нужно: удаляет существующие узлы и может создать XSS при недоверенных данных.container.innerHTML = items.map((item) => `<li>${item}</li>`).join('');​

Атрибут hidden меняет только видимость существующих элементов. Ссылки, обработчики и семантика сохраняются.

Если элементы приходят из API, создавайте узлы и записывайте пользовательский текст через textContent:

JavaScript
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 не загрузился.

Сообщить об ошибке в статье

Рекомендации редакции

Другие публикации из этого раздела.

Новые материалы

Получайте практические разборы без информационного шума

Одна общая рассылка о новых статьях и инструментах. Адрес будет активироваться только после подтверждения.

Подписка появится после подключения сервиса рассылок и Yandex SmartCaptcha.