Недавно я искал способ стилизовать элемент в зависимости от того, получал ли он когда-либо фокус.
Мне хотелось сделать это без JavaScript, только на CSS, потому что я собирался использовать этот приём в демо про новую возможность веб-платформы — focusgroup. focusgroup берёт на себя большую часть логики клавиатурной навигации — бесплатно и всего лишь с помощью HTML-атрибута. Поэтому добавлять в это демо пачку JavaScript-кода только ради отслеживания прошлых состояний фокуса казалось шагом назад.
Я долго крутил эту задачу в голове, но не находил решения. Сохранить состояние клика легко: можно использовать checkbox и затем применять стили на основе того, совпадает ли его псевдокласс :checked. Можно даже скрыть сам checkbox и показывать только связанный с ним элемент <label>, если так удобнее. Более того, checkbox вообще можно расположить где угодно, а затем с помощью псевдокласса :has() стилизовать другие элементы в зависимости от его состояния.
Но ничего похожего для отслеживания того, был ли элемент в фокусе, я найти не смог. И тут меня осенило: а что если использовать для этого CSS-анимацию?
Используем CSS-анимации как конечные автоматы
CSS-анимации по сути работают как конечные автоматы. Они могут изменять значение любого свойства с течением времени. Хитрость в том, чтобы перевести время в нужную точку, дойти до состояния, которое вы хотите запомнить, и затем зафиксировать его.
К счастью, у CSS-анимаций есть два очень полезных свойства:
animation-play-state, с помощью которого можно поставить анимацию на паузу в начальном состоянии.animation-fill-mode: forwards, с помощью которого можно сохранить анимацию в конечном состоянии после завершения.
Давайте используем их, чтобы настроить нашу анимацию:
.remember-focus {
animation-name: remember-focus;
animation-duration: .00001s;
animation-timing-function: linear;
animation-fill-mode: forwards;
animation-play-state: paused;
}Или в сокращённой записи:
.remember-focus {
animation: remember-focus .00001s linear forwards paused;
}Класс .remember-focus назначает элементу анимацию, пока оставляя её на паузе, но с fill-mode: forwards, чтобы после запуска и завершения сохранилось конечное состояние.
Обратите внимание на странно короткую длительность анимации — .00001s. Это потому, что мы хотим, чтобы анимация достигала конечного состояния сразу после запуска. Длительность должна быть настолько маленькой, чтобы для пользователя всё происходило мгновенно.
Когда наступит момент сменить состояние, всё, что нам нужно сделать, — запустить анимацию. Допустим, мы хотим сделать это, когда элемент получает фокус от пользователя:
.remember-focus:focus {
animation-play-state: running;
}Теперь остаётся только определить саму анимацию через правило @keyframes. Для простоты пока воспользуемся цветами фона:
@keyframes remember-focus {
from {
background: red;
}
to {
background: blue;
}
}Вот и всё. По умолчанию элемент, к которому применён класс .remember-focus, будет иметь красный фон. Когда он получит фокус, анимация запустится и сразу изменит цвет фона на синий. Благодаря animation-fill-mode: forwards элемент останется синим даже после потери фокуса.
Самое классное, что состояние анимации привязано к самому элементу. Поэтому даже если класс .remember-focus есть у нескольких элементов, каждый из них будет независимо помнить собственное состояние фокуса.
Демонстрации
Вот живое демо кода, который мы только что разобрали. Кликните по блоку или перейдите к нему через Tab, чтобы дать ему фокус: цвет изменится. И он останется таким даже после того, как вы кликнете в другое место или перейдёте к другому элементу через Tab:
<style>
.remember-focus {
padding: .5rem;
border: 2px dashed black;
inline-size: max-content;
color: white;
font-weight: bold;
font-size: 1.1rem;
animation: remember-focus .00001s linear forwards paused;
}
.remember-focus:focus {
animation-play-state: running;
}
@keyframes remember-focus {
from {
background: red;
}
to {
background: blue;
}
}
</style>
<div class="remember-focus" tabindex="0">Click or tab to focus me</div>Этот приём работает и с псевдоклассом :hover. Достаточно заменить селектор на .remember-focus:hover, и анимация будет запускаться при наведении, а не при фокусе.
Вот демо, которое использует hover вместо focus и содержит несколько элементов с одним и тем же классом. Наведите курсор на ячейки ниже и посмотрите, как они меняют цвет:
<style>
.hover-grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 1px;
max-block-size: 50vh;
aspect-ratio: 1;
}
.hover-grid div {
animation: remember-hover .00001s linear forwards paused;
}
.hover-grid div:hover {
animation-play-state: running;
}
@keyframes remember-hover {
from {
background: red;
}
to {
background: blue;
}
}
</style>
<div class="hover-grid">
<div></div><div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div><div></div>
<div></div><div></div><div></div><div></div><div></div>
</div>Анимация других свойств
Разумеется, изменение цвета — это только самый простой вариант применения. CSS-анимации умеют менять любые свойства, включая кастомные свойства, и даже те свойства, которые формально не анимируются.
Например, чтобы показать иконку рядом с элементом, который уже получал фокус, можно использовать этот приём для анимации свойства content у псевдоэлемента ::after, даже несмотря на то, что content не считается анимируемым свойством.
Попробуйте сами: сфокусируйте первый блок ниже, а затем с помощью Tab переходите по следующим. Вы увидите, как иконка рядом с уже сфокусированными блоками меняется:
<style>
.checks {
display: flex;
flex-wrap: wrap;
gap: .5rem;
}
.checks .check-on-focus {
padding: .5rem;
border: 2px dashed black;
inline-size: max-content;
cursor: pointer;
animation: check-on-focus .00001s linear forwards paused;
}
.checks .check-on-focus:hover {
background: #eee;
}
.checks .check-on-focus::after {
content: "🐰";
margin-inline-start: .25rem;
animation: check-on-focus .00001s linear forwards paused;
}
.checks .check-on-focus:focus {
animation-play-state: running;
}
.checks .check-on-focus:focus::after {
animation-play-state: running;
}
@keyframes check-on-focus {
to {
content: "😺";
background: #c7ff6e;
}
}
</style>
<div class="checks">
<div class="check-on-focus" tabindex="0">Box 1</div>
<div class="check-on-focus" tabindex="0">Box 2</div>
<div class="check-on-focus" tabindex="0">Box 3</div>
<div class="check-on-focus" tabindex="0">Box 4</div>
<div class="check-on-focus" tabindex="0">Box 5</div>
</div>Используем style container queries как if
В завершение давайте немного переработаем код, чтобы его было проще использовать в разных местах.
Сначала применим этот приём, чтобы менять значение кастомного свойства --was-focused:
.track-focus {
--was-focused: false;
animation: track-focus .00001s linear forwards paused;
}
.track-focus:focus-within {
animation-play-state: running;
}
@keyframes track-focus {
to { --was-focused: true; }
}Теперь мы можем использовать это, назначая класс .track-focus любому элементу, для которого хотим отслеживать состояние фокуса. Например, паре label внутри формы с инпутами:
<form class="my-form">
<label class="track-focus" for="name">
Your name
<input type="text" id="name">
</label>
<label class="track-focus" for="email">
Your email
<input type="text" id="email">
</label>
</form>Дальше, чтобы реально использовать свойство --was-focused, можно прибегнуть к container style query. Так мы сможем условно применять стили к самому элементу или к любому из его потомков в зависимости от того, получал ли он фокус хотя бы раз:
@container style(--was-focused: true) {
input {
background: lightgreen;
}
}А вот и результат:
<style>
.my-form {
padding: 1rem;
border: 2px dashed black;
}
.track-focus {
--was-focused: false;
animation: track-focus .00001s linear forwards paused;
}
.track-focus:focus-within {
animation-play-state: running;
}
@keyframes track-focus {
to { --was-focused: true; }
}
@container style(--was-focused: true) {
input {
background: lightgreen;
}
}
</style>
<form class="my-form">
<label class="track-focus" for="name">
Your name
<input type="text" id="name">
</label>
<label class="track-focus" for="email">
Your email
<input type="text" id="email">
</label>
</form>Вот и всё. Если придумаете классный сценарий применения этого приёма или у вас появятся идеи, как его улучшить, — обязательно поделитесь.
Теги: css, animation, focus