x
 
Кирилл Томарев
22 марта 2018
Советы почтой каждую неделю
Пожалуйста, получите наше письмо, чтобы подтвердить свой адрес:
Вы подписаны на «Советы за неделю»:

Привет, бюро!

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


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

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

Если же скорректировать дизайн невозможно, запасаемся временем, вооружаемся яваскриптом и приступаем.

Для начала соберём тестовый стенд. Для простоты возьмём слайдер с фиксированной шириной и известным количество слайдов. Также я усилю рамки и тени в изобразительных целях:

<!-- index.html -->
<div class="slider">
  <div class="slider-holder">
    <div class="slider-list">
      <div class="card">A</div>
      <div class="card">B</div>
      <div class="card">C</div>
      <!-- ... -->
    </div>
  </div>
  <button class="slider-pager">
    Перелистнуть
  </button>
</div>
/* style.css */
.slider {
  width: 600px;
}

/* Обрезаем «невидимую» часть списка */
.slider-holder {
  overflow: hidden;
}

/* Жёстко задаём ширину прокручивающейся
части с запасом */
.slider-list {
  display: flex;
  width: 10000%;
  transition: transform .3s;
}

.card {
  cursor: pointer;
  box-sizing: border-box;
  width: 200px;
  height: 200px;
  border: 1px solid #efefef;
  transition: box-shadow .3s;
}

/* При наведении пытаемся показать тень */
.card:hover {
  box-shadow: 0 0 2px rgba(0, 0, 0, .2);
}
// script.js
const SLIDER_WIDTH = 600
const VISIBLE_SLIDES = 3
const PAGES_NUMBER = 3
const PAGE_OFFSET = SLIDER_WIDTH

const $slider = document.querySelector('.slider')
const $list = $slider.querySelector('.slider-list')
const $pager = $slider.querySelector('.slider-pager')

let page = 0

// При нажатии на кнопку сдвигаем список на ширину
// одной «страницы». Если докрутили до конца,
// перематываем на начало
$pager.addEventListener('click', function() {
  page += 1
  if (page === PAGES_NUMBER) page = 0

  $list.style.transform = `translateX(-${page * PAGE_OFFSET}px)`
})
A
B
C
D
E
F
G
H
I

Получилась фигня: рамки между соседями складываются, а тени оберзаются родительским элементом и соседними карточками.

Для начала победим утолщение рамок. Для этого наложим карточки друг на друга на ширину рамки:

/* style.css */
.slider {
  width: 598px;
}

.card {
  margin-left: -1px;
}

.card:first-child {
  margin-left: 0;
}
// script.js
const SLIDER_WIDTH = 598
// Из-за наложения карточек друг на друга
// смещать список теперь нужно на пиксель меньше ширины слайдера
const PAGE_OFFSET = SLIDER_WIDTH - 1

// ...

A
B
C
D
E
F
G
H
I

Теперь разберёмся с обрезанием тени.

Проблема в том, что для того, чтобы скрыть «невидимую» часть списка, мы обрезаем всё, что вылезает за пределы слайдера с помощью overflow: hidden.

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

// script.js
const SLIDER_WIDTH = 598
const VISIBLE_SLIDES = 3
const PAGE_OFFSET = SLIDER_WIDTH - 1

const $slider = document.querySelector('.slider')
const $list = $slider.querySelector('.slider-list')
const $pager = $slider.querySelector('.slider-pager')
const $cards = $list.querySelectorAll('.card')

// Создадим контейнер для списка-клона
// и назначим ему нужные классы
const $listClone = document.createElement('div')
$listClone.classList.add('slider-list', 'is__clone')

Array.from($cards).forEach(function($card) {
  // Создадим клон для каждой карточки
  // и поместим его в список-клон
  const $cardClone = document.createElement('div')
  $cardClone.classList.add('card')
  $listClone.append($cardClone)

  // При наведении на оригинальную карточку
  // добавляем класс `is__hovered` клонированной
  $card.addEventListener('mouseover', () =>
    $cardClone.classList.add('is__hovered'))
  $card.addEventListener('mouseout', () =>
    $cardClone.classList.remove('is__hovered'))
})

// Добавим список-клон в слайдер
$slider.append($listClone)

let page = 0
$pager.addEventListener('click', function() {
  page += 1
  if (page === VISIBLE_SLIDES) page = 0

  // При листании одновременно двигаем
  // оригинальный и клонированный списки
  $list.style.transform =
    `translateX(-${page * PAGE_OFFSET}px)`
  $listClone.style.transform =
      `translateX(-${page * PAGE_OFFSET}px)`
})
/* style.css */
/* Разместим клонированный список
поверх основного */
.slider.is__clone {
  position: absolute;
  top: 0;
  left: 0;

  /* Чтобы этот список не мешал наводить
  и кликать на оригинальные карточки,
  отключим у него взаимодействие с мышью */
  pointer-events: none;
}

/* Перенесём тень на клонированную карточку */
.slider-list.is__clone .card {
  opacity: 0;
  box-shadow: 0 0 2px rgba(0,0,0,.2);
}

.card.is__hovered {
  opacity: 1;
}

A
B
C
D
E
F
G
H
I

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

P. S. Это был совет о веб-разработке. Хотите знать всё о коде, тестах, фронтенд-разработке, цеэсэсе, яваскрипте, рельсах и джейде? Присылайте вопросы.
Вёрстка и прототипирование — дисциплина Школы дизайнеров. Набор открыт. Чем раньше поступите, тем ниже стоимость и выше шанс на бесплатное место.
 

Поделиться
Отправить

Комментарии

Дима Шишкин
22 марта 2018

width: 10000%; — так себе решение. Конечно в вебе такое практикуется и в теории сломаться не должно, но можно и без магических чисел:

.slider-list {
  display: flex;
  transition: transform .3s;
}

.card {
  flex-shrink: 0;
}

Павел Воронов
20 ноября 2018

А если попробовать сделать проще:
1. Добавить .slider внутренние отступы, например на 10рх, чтобы overflow не обрезало тень сверху и краев.
2. Но тогда появятся края следующего блока.
3. Прячем их через псевдоэлементы, добавленные к блоку .slider-list спозиционированных абсолютно + z-index. Так как relative у нас лежит на .slider, псевдоэлементы перепрыгнут через overflow и расположатся по краям, закрывая выступающую плитку.
4. Сам эффект тени реализуем через z-index. При ховере на .card задаём его больше чем у псевдоэлементов, а плавности достигаем за счёт анимирования появления тени.

Не нужно никого клонировать и синхронизировать.

П. С. Этот пример реализовывать не пробовал, так что, возможно, это ложь и клевета :-)


Цель рубрики — обсуждение вопросов дизайна всех видов, текста в дизайне и взаимоотношений дизайнеров с клиентами.

Мы публикуем комментарии, которые добавляют к уже сказанному новые мысли и хорошие примеры. Мы ожидаем, что такие комментарии составят около 20% от общего числа.

Решение о публикации принимается один раз; мы не имеем возможности комментировать или пересматривать свое решение, хотя оно может быть ошибочно. Уже опубликованные комментарии могут быть удалены через некоторое время, если без них обсуждение не становится менее ценным или интересным.

Вот такой веб 2.0.

Как проверять соответствие сайта макету в 2024? Какие есть аналоги PixelPerfect? Ошибки вложенности в ХТМЛ 1 Откуда берётся отступ под картинкой и как его убрать? 1 Как добавить видео на сайт?




Недавно всплыло

О тексте как базовом элементе 6 2 Всё просто 2 Как правильно писать диапазон дат? 2