CSS Веерное раскрытие с grid
и @property
веерное раскрытиеподразумевает последовательное раскрытие элементов из стопки с эффектом
подпрыгивания. Используя CSS grid, мы избавляемся от лишней работы по позиционированию.
Веерное раскрытие
— это расширяющаяся анимация, при которой группа предметов появляется один за другим, рядом друг с другом, как будто их выкладывают из стопки. Как правило, при этом наблюдается едва уловимое подпрыгивание.
Эффект обычно достигается за счёт тайминга и позиционирования каждого элемента по отдельности с очень жёстко заданными значениями. Однако это может оказаться очень трудоёмкой задачей. Можно сделать всё немного проще, если позволить родительскому контейнеру элементов сделать это за нас. Вот результат, полученный таким образом:
В HTML, это группа элементов (плюс пустой элемент — позже я объясню, зачем он нужен), окружённая двумя элементами <input type='radio'>
для открытия и закрытия элементов соответственно.
<section class="items-container">
<p class="items"><!--empty--></p>
<label class="items close">
Close the messages<input type="radio" name="radio">
</label>
<p class="items">Alert from Project X</p>
<p class="items">🐩 Willow's appointment at <i>Scrubby's</i></p>
<p class="items">Message from (-_-)</p>
<p class="items">NYT Feed: <u>Weather In... (Read more)</u></p>
<p class="items">6 more items to check in your vacation list!</p>
<label class="items open">
Show the messages<input type="radio" name="radio">
</label>
</section>
Нам нужен контейнер grid
, чтобы это работало, поэтому давайте превратим <section>
, контейнер с элементами, в такой элемент. Можно использовать список или любой другой элемент, который кажется вам семантически подходящим.
.items-container {
display: grid;
}
Теперь создадим CSS переменную integer
со значением, равным количеству элементов внутри контейнера (включая открытые и закрытые элементы управления, а также пустой элемент). Это необходимо для того, чтобы реализовать последовательное раскрытие и скрытие элементов в стилевом правиле контейнера grid.
Также зарегистрируем ещё одну CSS переменную с типом данных length
, используемую для анимирования высоты каждого элемента при открытии и закрытии элемента управления, для более плавного выполнения общего действия.
@property --int {
syntax: "<integer>";
inherits: false;
initial-value: 7;
}
@property --hgt {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
Используем созданные CSS переменные --int
и --hgt
, чтобы добавить в контейнер grid необходимое количество строк grid с нулевой высотой.
.items-container {
display: grid;
grid-template-rows: repeat(calc(var(--int)), var(--hgt));
}
При прямом добавлении --int
к repeat()
в Safari получалась пятнистая анимация, поэтому я прогнал её через calc()
, и анимация заработала (мы рассмотрим её в ближайшее время). Однако вычисления calc()
продолжали пропускать один элемент в итерации из-за того, как вычислялось значение 0. Поэтому пришлось добавить пустой элемент, компенсирующий это исключение.
Если бы Safari не давал клякс, мне бы не понадобился пустой элемент, --int
initial-value
было бы 6, а значение grid-template-rows
было бы просто repeat(var(--int), 0px)
. На самом деле, при такой настройке я получил хорошие результаты анимации как в Firefox, так и в Chrome.
Однако в итоге я выбрал вариант с использованием calc()
, обеспечивший желаемый результат во всех основных браузерах.
Теперь перейдём к анимации:
@keyframes open { to { --int: 0; --hgt:60px;} }
@keyframes close { to { --int: 6; --hgt:0px;} }
.item-container {
display: grid;
grid-template-rows: repeat(calc(var(--int)), var(--hgt));
&:has(.open :checked) {
/* open action */
animation: open .3s ease-in-out forwards;
.open { display: none; }
}
&:has(.close :checked) {
/* close action */
--int: 0;
--hgt: 60px;
animation: close .3s ease-in-out forwards;
}
}
Когда input
находится в состоянии checked
, выполняется keyframe-анимация open
, а сам элемент управления скрывается с помощью display: none
.
Класс open
изменяет значение --int
с его initial-value
, 7
, до значения, установленного в правиле @keyframes
(0
), в течение заданного периода времени (.3s
). Этот декремент удаляет нулевую высоту из каждого ряда grid, один за другим, таким образом последовательно раскрывая все элементы за .3s
или 300ms
. Одновременно значение --hgt
увеличивается до 60px
с начального значения 0px
. Это увеличивает высоту каждого элемента по мере его появления на экране.
Когда input
для скрытия всех элементов находится в состоянии checked
, выполняется keyframe-анимация close
, устанавливающая значение --int
в 0
и значение --hgt
в 60px
.
Класс close
изменяет значение --int
, которое сейчас равно 0
, на значение, объявленное в его правиле: 7
. Это увеличение устанавливает нулевую высоту для каждой строки grid, по очереди, таким образом последовательно скрывая все элементы. Одновременно значение --hgt
уменьшается до 0px
. Это уменьшает высоту каждого элемента по мере его исчезновения с экрана.
Для выполнения действия закрытия, вместо того чтобы создавать уникальную анимацию закрытия, я попробовал использовать анимацию открытия с animation-direction: reverse
. К сожалению, результат получился прерывистым. Поэтому я сохранил уникальные анимации для действий открытия и закрытия отдельно.
Кроме того, чтобы отполировать пользовательский интерфейс, я добавил анимацию переходов для промежутков между строками и цвета текста. В промежутках между строками установлена функция синхронизации анимации cubic-bezier()
для создания слабого пружинящего эффекта.
.scroll-container {
display: grid;
grid-template-rows: repeat(calc(var(--int)), 0px); /* serves the open and close actions */
transition: row-gap .3s .1s cubic-bezier(.8, .5, .2, 1.4);
&:has(.open :checked) {
/* open action */
animation: open .3s ease-in-out forwards;
.open { display: none; }
/* styling */
row-gap: 10px;
.items { color: rgb(113 124 158); transition: color .3s .1s;}
.close { color: black }
}
&:has(.close :checked) {
/* close action */
--int: 0;
animation: close .3s ease-in-out forwards;
/* styling */
row-gap: 0;
.items { color: transparent; transition: color .2s;}
}
}
При расширении строки gap
увеличивается до 10px
, а цвет текста становится прозрачным. При сжатии строки gap
уменьшается до 0
, а цвет текста становится прозрачным. На этом пример завершён! Вот Pen ещё раз:
Дополнительные материалы
grid-template-rows
— MDN Web Docs@property
: Next-gen CSS variables now with universal browser support — web.dev- CSS easing function — MDN Web Docs