Древовидные представления, только html и css

Древовидное представление (сворачиваемый список) можно сделать, используя только html и css, без использования JavaScript.

Программное обеспечение для специальных возможностей будет показывать дерево в виде списков, вложенных в раскрывающиеся виджеты, а взаимодействие с клавиатурой поддерживается автоматически.

See this code tree-views html+css only on x.xhtml.ru.

HTML

HTML разметка начинается с простых вложенных списков:

<ul>
  <li>
    Планеты гиганты
    <ul>
      <li>
        Газовые
        <ul>
          <li>Юпитер</li>
          <li>Сатурн</li>
        </ul>
      </li>
      <li>
        Ледяные
        <ul>
          <li>Уран</li>
          <li>Нептун</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

Затем надо добавить CSS-класс самому внешнему элементу <ul>. Внутренности каждого элемента списка, содержащего свой вложенный список, поместить внутрь элемента <details> и добавить <summary>. Атрибут open e элемента <details> будет использоваться для управления вложенными элементами. Список верхнего уровня изначально раскрыт:

<ul class="tree">
  <li>
    <details open>
      <summary>Планеты гиганты</summary>
      <ul>
        <li>
          <details>
            <summary>Газовые</summary>
            <ul>
              <li>Юпитер</li>
              <li>Сатурн</li>
            </ul>
          </details>
        </li>
        <li>
          <details>
            <summary>Ледяные</summary>
            <ul>
              <li>Уран</li>
              <li>Нептун</li>
            </ul>
          </details>
        </li>
      </ul>
    </details>
  </li>
</ul>

Без CSS это будет выглядеть так:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Пока ничто не говорит пользователю, что список можно разворачивать и открывать новые уровни, сочетание маркеров представляет запутанный пользовательский интерфейс, но это дерево уже можно раскрывать. Браузер реализует элемент <details>, как раскрывающийся виджет, предоставляя возможность разворачивать и сворачивать вложенные списки.

Пользовательские свойства

На компоновку древовидного представления влияют два параметра: межстрочный интервал (равный высоте строки текста) и радиус маркеров. Мы начнем с создания пользовательских CSS-свойств для определения этих размеров:

.tree{
  --spacing : 1.5rem;
  --radius  : 10px;
}

Хотя обычно используются относительные единицы для масштабирования элементов управления пользовательского интерфейса на основе размера текста, для маркеров это может привести к тому, что элементы управления будут слишком маленькими или чрезмерно большими. Поэтому здесь будет использоваться разумный фиксированный размер 10px.

Отступы

Теперь стилизуются элементы основного и вложенных списков, чтобы подготовить место для линий и маркеров:

.tree li{
  display      : block;
  position     : relative;
  padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
}

.tree ul{
  margin-left  : calc(var(--radius) - var(--spacing));
  padding-left : 0;
}

Для селектора .tree li CSS-свойство display: block; удалит маркеры из элементов списка. position: relative; установит новый контекст стека и содержимого блока, который будет использоваться для размещения линий и маркеров.

padding-left cделает отступы для элементов списка. Отступ равен удвоенному интервалу минус радиус маркера минус ширина линии в два пикселя. В результате текст в элементе списка будет выровнен по левой стороне маркера под ним.

Для селектора .tree ul CSS-свойство margin-left компенсирует отступы селектора .tree li, а padding-left сбросит отступы, которые браузер устанавливает спискам по умолчанию.

Для древовидного представления с развернутыми вложенными списками применение этого стиля дает:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Вертикальные линии

Вертикальные линии, которые образуют часть линий, соединяющих маркер каждого элемента списка с маркерами его вложенных списков:

.tree ul li{
  border-left : 2px solid #ddd;
}

.tree ul li:last-child{
  border-color : transparent;
}

Для вертикальных линий используется CSS-свойство border-left, кроме последнего элемента в каждом списке, где рамка скрывается, поскольку линия не должна продолжаться за маркером этого элемента. Делая границу прозрачной, а не удаляя её полностью, можно избежать необходимости увеличивать отступ для компенсации выравнивания.

Применение этого стиля дает:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Горизонтальные линии

Чтобы добавить горизонтальные линии, соединяющие вертикальные линии с маркерами каждого элемента списка, используется сгенерированный контент:

.tree ul li::before{
  content      : '';
  display      : block;
  position     : absolute;
  top          : calc(var(--spacing) / -2);
  left         : -2px;
  width        : calc(var(--spacing) + 2px);
  height       : calc(var(--spacing) + 1px);
  border       : solid #ddd;
  border-width : 0 0 2px 2px;
}

Этот код создает ещё и короткие вертикальные линии, так как созданные ранее вертикальные линии не доходят до маркеров на их верхнем и нижнем концах.

Здесь создаётся блок и располагается так, чтобы начало оказалось в середине предыдущей строки текста, перекрывая вертикальную линию слева от неё.

Размеры блока должны быть на два пикселя шире интервала, чтобы он перекрывал вертикальную линию слева от него и на один пиксель выше, поскольку половина ширины горизонтальной линии находится ниже середины строки текста. Замечание: здесь используется box-sizing: border-box, поэтому размеры включают рамку.

Применение этого стиля дает:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

HTML-элемент summary

Далее следует удалить стиль по умолчанию у HTML-элемента summary:

.tree summary{
  display : block;
  cursor  : pointer;
}

.tree summary::marker,
.tree summary::-webkit-details-marker{
  display : none;
}

.tree summary:focus{
  outline : none;
}

.tree summary:focus-visible{
  outline : 1px dotted #000;
}

здесь будут удалены маркеры-стрелки и установлен курсор, чтобы обозначить возможность взаимодействовать с элементом — кликабельность.

Safari показывает индикатор фокуса вокруг summary даже если используется мышь, а не клавиатура. Поэтому здесь сперва удаляется стиль фокуса, а затем используется псевдокласс :focus-visible, чтобы добавить его обратно для посетителей, использующих навигацию с помощью клавиатуры.

Применение этого стиля дает:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Маркеры

Здесь, чтобы сделать маркеры, снова используется генерируемый контент:

.tree li::after,
.tree summary::before{
  content       : '';
  display       : block;
  position      : absolute;
  top           : calc(var(--spacing) / 2 - var(--radius));
  left          : calc(var(--spacing) - var(--radius) - 1px);
  width         : calc(2 * var(--radius));
  height        : calc(2 * var(--radius));
  border-radius : 50%;
  background    : #ddd;
}

Маркеры генерируются для элементов <li> (которые не содержат вложенных списков) и для элементов <summary>. Это позволяет элементам, содержащим вложенные списки, иметь иной стиль маркеров в зависимости от того, свернут или раскрыт вложенный список.

Здесь формируется блок, отцентрированный над пересечением горизонтальной и вертикальной линиями. Верх располагается в середине текстовой строки за вычетом радиуса. Левая сторона располагается на краю вертикальной линии минус радиус минус один пиксель, соответствующий половине ширины линии.

Применение этого стиля дает:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Кнопки сворачивания и раскрытия списков

Наконец, осталось добавить кнопки сворачивания и раскрытия древовидных списков:

.tree summary::before{
  content     : '+';
  z-index     : 1;
  background  : #696;
  color       : #fff;
  line-height : calc(2 * var(--radius) - 2px);
  text-align  : center;
}

.tree details[open] > summary::before{
  content : '−';
}

Обратите внимание, что используется настоящий знак минус (−), а не дефис (-). Он соответствует внешнему виду знака плюс, тогда как в большинстве шрифтов дефис уже и ниже.

В зависимости от используемого шрифта, может потребоваться коррекция положения символов, например - 2px у CSS-свойства line-height.

Применение этого стиля приводит к готовому древовидному представлению:

  • Планеты гиганты
    • Газовые
      • Юпитер
      • Сатурн
    • Ледяные
      • Уран
      • Нептун

Готовый код

.tree{
  --spacing : 1.5rem;
  --radius  : 10px;
}

.tree li{
  display      : block;
  position     : relative;
  padding-left : calc(2 * var(--spacing) - var(--radius) - 2px);
}

.tree ul{
  margin-left  : calc(var(--radius) - var(--spacing));
  padding-left : 0;
}

.tree ul li{
  border-left : 2px solid #ddd;
}

.tree ul li:last-child{
  border-color : transparent;
}

.tree ul li::before{
  content      : '';
  display      : block;
  position     : absolute;
  top          : calc(var(--spacing) / -2);
  left         : -2px;
  width        : calc(var(--spacing) + 2px);
  height       : calc(var(--spacing) + 1px);
  border       : solid #ddd;
  border-width : 0 0 2px 2px;
}

.tree summary{
  display : block;
  cursor  : pointer;
}

.tree summary::marker,
.tree summary::-webkit-details-marker{
  display : none;
}

.tree summary:focus{
  outline : none;
}

.tree summary:focus-visible{
  outline : 1px dotted #000;
}

.tree li::after,
.tree summary::before{
  content       : '';
  display       : block;
  position      : absolute;
  top           : calc(var(--spacing) / 2 - var(--radius));
  left          : calc(var(--spacing) - var(--radius) - 1px);
  width         : calc(2 * var(--radius));
  height        : calc(2 * var(--radius));
  border-radius : 50%;
  background    : #ddd;
}

.tree summary::before{
  content     : '+';
  z-index     : 1;
  background  : #696;
  color       : #fff;
  line-height : calc(2 * var(--radius) - 2px);
  text-align  : center;
}

.tree details[open] > summary::before{
  content : '−';
}