Градиентная прозрачность карточек, трансформация курсора при наведении

В данной статье рассмотрим как добиться градиентной прозрачности карточек и сделаем эффект трансформации курсора при наведении на карточку

HTML структура

Напишем простейшую структуру для примера

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
    <title>Cards</title>
    <link rel="stylesheet" href="css/bootstrap-reboot.min.css"/>
    <link rel="preconnect" href="https://fonts.gstatic.com"/>
    <link href="https://fonts.googleapis.com/css2?family=Teko:wght@300&amp;display=swap" rel="stylesheet"/>
    <link rel="stylesheet" href="css/main.css"/>
  </head>
  <body>
    <div class="section">
      <div class="container">
        <div class="section__grid">
          <div class="section__shape shape"></div>
          <div class="section__card card">
            <div class="card__image"><img src="https://images.unsplash.com/photo-1566480047210-b10eaa1f8095?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1000&amp;q=80" alt=""/></div>
            <div class="card__title">mountain bike <b>RUDE</b></div>
          </div>
          <div class="section__card card">
            <div class="card__image"><img src="https://images.unsplash.com/photo-1574117482334-14b040604998?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&amp;ixlib=rb-1.2.1&amp;auto=format&amp;fit=crop&amp;w=1000&amp;q=80" alt=""/></div>
            <div class="card__title">mountain bike <b>GEAR</b></div>
          </div>
        </div>
      </div>
    </div>
    <script src="js/main.js"></script>
  </body>
</html>


CSS стили

За градиентную прозрачность отвечает свойство mask-image, которое может принимать в качестве значения градиенты, а также svg, png изображения

.card__image {
  position: absolute;
  z-index: 0;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  width: 100%;
  height: 100%;
  -webkit-mask-image: linear-gradient(#222 48%, transparent);
  mask-image: linear-gradient(#222 48%, transparent);
}


Чтобы увидеть градиентную прозрачность, стилизуем и анимируем круг на заднем плане

.shape {
  -webkit-animation: move 3s ease-in-out infinite alternate;
  animation: move 3s ease-in-out infinite alternate;
  --size: 600px;
  position: absolute;
  width: var(--size);
  height: var(--size);
  border: 8px solid #e03616;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  opacity: 0.64;
  transform: translateY(-50%) translateX(-50%);
}
@-webkit-keyframes move {
  0% {
    opacity: 0.64;
    transform: translateY(-50%) translateX(-50%) scale(0.9);
  }
  100% {
    opacity: 0.8;
    transform: translateY(-50%) translateX(-50%) scale(1);
  }
}
@keyframes move {
  0% {
    opacity: 0.64;
    transform: translateY(-50%) translateX(-50%) scale(0.9);
  }
  100% {
    opacity: 0.8;
    transform: translateY(-50%) translateX(-50%) scale(1);
  }
}


Чтобы убрать курсор при наведении на карточку используем cursor: none;

Добавим полупрозрачный круг через псевдоэлемент :before, назначим ему свойства left и top со значениями CSS переменных --x и --y, которые он будет брать от родительского элемента (динамику напишем на JavaScript чуть ниже). Начальный размер псевдоэлемента будет равен 0px, а при наведении на карточку увеличиваем размер до 300px. Для плавного изменения размера используем transition: width 0.48s ease, height 0.48s ease;. Чтобы центр круга совпадал с координатами курсора назначаем transform: translate(-50%, -50%);

.card:before {
  --size: 0px;
  left: var(--x);
  top: var(--y);
  width: var(--size);
  height: var(--size);
  content: '';
  position: absolute;
  transform: translate(-50%, -50%);
  transition: width 0.48s ease, height 0.48s ease;
  z-index: 1;
  border-radius: 50%;
  background: rgba(255,0,0,0.24);
}


Полный CSS код примера

body {
  font-family: 'Teko', sans-serif;
  background: #fff;
  position: relative;
  background: #000;
  display: grid;
}
.section {
  background: #222;
  overflow: hidden;
}
.section__grid {
  z-index: 1;
  display: grid;
  place-items: center;
  min-height: 100vh;
  grid-template-columns: 1fr 1fr;
  grid-gap: 32px;
  gap: 32px;
  padding: 64px 0;
}
.container {
  max-width: 640px;
  margin: 0 auto;
  padding: 0 16px;
  z-index: 1;
  position: relative;
}
.card {
  box-shadow: 0 4px 8px #111;
  background: rgba(0,0,255,0.48);
  padding: 32px;
  border-radius: 32px;
  width: 100%;
  min-height: 400px;
  display: flex;
  align-items: flex-end;
  color: #fff;
  overflow: hidden;
  position: relative;
  cursor: none;
  transition: background 0.48s ease, box-shadow 0.48s ease;
  border: 4px solid #111;
}
.card:before {
  --size: 0px;
  left: var(--x);
  top: var(--y);
  width: var(--size);
  height: var(--size);
  content: '';
  position: absolute;
  transform: translate(-50%, -50%);
  transition: width 0.48s ease, height 0.48s ease;
  z-index: 1;
  border-radius: 50%;
  background: rgba(255,0,0,0.24);
}
.card:hover {
  box-shadow: 0 8px 32px #111;
}
.card:hover:before {
  --size: 300px;
}
.card__image {
  position: absolute;
  z-index: 0;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  width: 100%;
  height: 100%;
  -webkit-mask-image: linear-gradient(#222 48%, transparent);
  mask-image: linear-gradient(#222 48%, transparent);
}
.card__image img {
  width: 100%;
  height: 100%;
  -o-object-fit: cover;
  object-fit: cover;
  display: block;
}
.card__title {
  z-index: 1;
  pointer-events: none;
  font-size: 24px;
  text-align: right;
  width: 100%;
  color: rgba(255,255,255,0.48);
}
.card__title b {
  font-size: 32px;
  margin-left: 8px;
  color: #fff;
}
.shape {
  -webkit-animation: move 3s ease-in-out infinite alternate;
  animation: move 3s ease-in-out infinite alternate;
  --size: 600px;
  position: absolute;
  width: var(--size);
  height: var(--size);
  border: 8px solid #e03616;
  border-radius: 50%;
  top: 50%;
  left: 50%;
  opacity: 0.64;
  transform: translateY(-50%) translateX(-50%);
}
@media (max-width: 639.98px) {
  .section__grid {
    grid-template-columns: 1fr;
  }
}
@-webkit-keyframes move {
  0% {
    opacity: 0.64;
    transform: translateY(-50%) translateX(-50%) scale(0.9);
  }
  100% {
    opacity: 0.8;
    transform: translateY(-50%) translateX(-50%) scale(1);
  }
}
@keyframes move {
  0% {
    opacity: 0.64;
    transform: translateY(-50%) translateX(-50%) scale(0.9);
  }
  100% {
    opacity: 0.8;
    transform: translateY(-50%) translateX(-50%) scale(1);
  }
}


JavaScript код

Возьмем идею следования за курсором с крутого ресурса - https://www.30secondsofcode.org/css/s/mouse-cursor-gradient-tracking

Немного преобразуем код для нашей задачи

Логика следующая - при наведении на карточку вычисляем координаты курсора и назначаем эти значения в соответствующие CSS переменные. Элемент, который будет заменять курсор (в нашем случае это полупрозрачный круг) получает значения этих CSS переменных от родительского элемента (то есть от карточки) и динамически меняет свое положение в зависимости от них

Подробнее про по getBoundingClientRect() - https://learn.javascript.ru/coordinates

Код с комментариями

document.addEventListener('DOMContentLoaded', () => { // структура документа загружена и готова к взаимодействию

  let cards = document.querySelectorAll('.card'); // получаем все карточки
  cards.forEach(card => { // для каждой карточки
    card.addEventListener('mousemove', e => { // добавляем событие передвижения курсора
      let rect = e.target.getBoundingClientRect(); // определяем координаты курсора
      let x = e.clientX - rect.left; // вычисляем координату x
      let y = e.clientY - rect.top; // вычисляем координату y
      card.style.setProperty('--x', x + 'px'); // назначаем значение CSS переменной --x
      card.style.setProperty('--y', y + 'px'); // назначаем значение CSS переменной --y
    });
  })

});


Получаем следующий результат

mountain bike RUDE
mountain bike GEAR


Архив с примером можно скачать по ссылке



Буду рад, если статья оказалась полезной

Спасибо за ваше внимание и уделённое время!