Компьютерная стрелочка передвигает окошки с изображениями
Иллюстрация: Кира Кустова

Позиционирование элементов с помощью JavaScript

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

Время чтения: 9 мин

Кратко

Скопировано

Элементы на странице можно позиционировать не только с помощью стилей, но и с помощью JavaScript. В этой статье мы рассмотрим ситуации, когда это оправдано и как таким позиционированием пользоваться.

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

Когда использовать стили

Скопировано

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

CSS — это инструмент, который специально был придуман для стилизации документов.

  • Это работает в среднем быстрее.
  • Это не собьёт с толку других разработчиков, которые будут читать ваш код.
  • Это разделяет зоны ответственности между скриптами и стилями.

Когда использовать скрипты

Скопировано

Используйте скрипты для позиционирования тогда, когда стилей не хватает.

CSS ограничен в обратной связи на действия пользователей на экране. В нём есть такие штуки как @keyframes, transition, :hover, :active, :focus и т. д., но этого не всегда достаточно.

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

Такие случаи — это не просто стилизация документа, а, скорее, смесь из стилизации и программной логики. Чтобы решить такую задачу, нам нужны как инструменты стилизации (CSS), так и инструменты для программирования логики (JavaScript).

Как менять позиционирование на скриптах

Скопировано

Изменять положение элементов на странице (как и любые стили элементов) можно несколькими способами.

Изменять классы

Скопировано

Допустим, мы хотим переместить элемент при клике на него в другое место. Для решения такой задачи нам вполне подойдёт способ с изменением класса у элемента.

Определим CSS-классы:

        
          
          .element {  /* Стили самого элемента */}.element-initial {  /* Стили, определяющие начальное положение    элемента на странице  */  transform: translateX(0px);}.element-final {  /* Стили, определяющие конечное положение */  transform: translateX(50px);}
          .element {
  /* Стили самого элемента */
}

.element-initial {
  /* Стили, определяющие начальное положение
    элемента на странице
  */
  transform: translateX(0px);
}

.element-final {
  /* Стили, определяющие конечное положение */
  transform: translateX(50px);
}

        
        
          
        
      

Элементу изначально заданы классы element element-initial, которые задают его стили, а также его начальное положение.

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

        
          
          // Обрабатываем событие клика на элементеelement.addEventListener('click', () => {  element.classList.toggle('element-final')  element.classList.toggle('element-initial')})
          // Обрабатываем событие клика на элементе
element.addEventListener('click', () => {
  element.classList.toggle('element-final')
  element.classList.toggle('element-initial')
})

        
        
          
        
      

Тогда получим элемент, который меняет своё положение при клике на него.

Открыть демо в новой вкладке

Этот способ изменять стили элемента с помощью скриптов самый простой и чистый — все стили остаются описанными внутри CSS. Однако он не всегда подходит.

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

Изменять style

Скопировано

Второй способ изменять положение элемента — менять атрибут style с помощью JavaScript.

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

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

Для изменения положения через style можно использовать разные свойства.

Изменение margin или top / left / right / bottom

Скопировано

Первое, что приходит на ум — изменение соответствующих CSS-свойств типа margin или left / top / right / bottom.

Создадим элемент с классом element:

        
          
          .element {  width: 50px;  height: 50px;  background: black;  position: absolute;}
          .element {
  width: 50px;
  height: 50px;
  background: black;
  position: absolute;
}

        
        
          
        
      

Теперь попробуем написать драг-н-дроп для мыши. Сперва создадим ссылку на этот элемент, чтобы обрабатывать события на нём. Переменная dragging будет отвечать за состояние элемента. Если его тащат, то переменная будет со значением true. По умолчанию она false. В переменных startX и startY будем держать координаты точки, в которой находился элемент, когда мы начали его тащить мышью.

При событии mousedown, когда на элемент нажимают мышью, мы отмечаем dragging как true — значит, элемент начали тащить. В значения для startX и startY помещаем положение курсора через свойства события e.pageX и e.pageY. Из положения курсора вычитаем отступы элемента, если они есть. Вычитание отступов нужно, чтобы элемент «запоминал» своё последнее положение, иначе мы всегда будем начинать тащить его от начала экрана.

Далее обрабатываем событие перемещения мыши по <body>. Мы наблюдаем именно за <body>, потому что хотим, чтобы изменения работали на всей странице, а не только внутри элемента element. Если элемент не тащат, ничего не делаем. Если тащат, то высчитываем новое положение, вычитая начальное положение элемента из положения курсора. Когда мы отпускаем мышь, отмечаем dragging как false.

        
          
          // Создаём ссылкуconst element = document.querySelector('.element')let dragging = false// Начальные координатыlet startX = 0let startY = 0// Событие при перетаскивании элементаelement.addEventListener('mousedown', (e) => {  dragging = true  startX = e.pageX - Number.parseInt(element.style.left || 0)  startY = e.pageY - Number.parseInt(element.style.top || 0)})// Обрабатываем событие перемещения мыши по <body>document.body.addEventListener('mousemove', (e) => {  // Элемент не перетаскивают  if (!dragging) return  // Элемент перетаскивают  element.style.top = `${e.pageY - startY}px`  element.style.left = `${e.pageX - startX}px`})// Отпускаем мышьdocument.body.addEventListener('mouseup', () => {  dragging = false})
          // Создаём ссылку
const element = document.querySelector('.element')

let dragging = false

// Начальные координаты
let startX = 0
let startY = 0

// Событие при перетаскивании элемента
element.addEventListener('mousedown', (e) => {
  dragging = true

  startX = e.pageX - Number.parseInt(element.style.left || 0)
  startY = e.pageY - Number.parseInt(element.style.top || 0)
})

// Обрабатываем событие перемещения мыши по <body>
document.body.addEventListener('mousemove', (e) => {
  // Элемент не перетаскивают
  if (!dragging) return

  // Элемент перетаскивают
  element.style.top = `${e.pageY - startY}px`
  element.style.left = `${e.pageX - startX}px`
})

// Отпускаем мышь
document.body.addEventListener('mouseup', () => {
  dragging = false
})

        
        
          
        
      

Тогда получится вот такой драг-н-дроп:

Открыть демо в новой вкладке

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

Как браузер рисует страницы

Мы можем сделать лучше.

Изменение transform

Скопировано

Перепишем наш драг-н-дроп, меняя теперь значение свойства transform.

Основа кода останется той же, стили и разметка не поменяются вовсе. В скриптах мы слегка изменим определение положения элемента.

В этот раз не сможем считать нужные значения напрямую. Вместо этого потребуется вначале вычислить стиль элемента через window.getComputedStyle(), а затем узнать значение свойства transform. Мы могли бы просто считать значение style.transform, но это бы не сильно помогло. При обычном считывании мы бы получили нечто вроде matrix(1, 0, 0, 1, 27, 15). Это матрица афинных преобразований. Её можно представить в виде matrix(scaleX, skewY, skewX, scaleY, translateX, translateY), где:

  • scaleX — масштабирование по горизонтали;
  • scaleY — масштабирование по вертикали;
  • skewX — перекос по горизонтали;
  • skewY — перекос по вертикали;
  • translateX — смещение по горизонтали;
  • translateY — смещение по вертикали.

Но, даже учитывая, что у нас есть все необходимые числа, работать с этим неудобно — это же просто строка. К счастью, можем воспользоваться DOMMatrixReadOnly, который преобразует эту матрицу в удобную для использования. После можем воспользоваться свойствами, которые содержат в себе значения translateX и translateY. Дальше — как раньше, только вычитаем не top и left, а translateX и translateY. Наконец, добавляем возможность отпустить элемент при отжатии клавиши.

        
          
          // ...element.addEventListener('mousedown', (e) => {  dragging = true  const style = window.getComputedStyle(element)  // Преобразуем матрицу  const transform = new DOMMatrixReadOnly(style.transform)  const translateX = transform.m41  const translateY = transform.m42  startX = e.pageX - translateX  startY = e.pageY - translateY})document.body.addEventListener('mouseup', () => {  dragging = false})
          // ...

element.addEventListener('mousedown', (e) => {
  dragging = true

  const style = window.getComputedStyle(element)

  // Преобразуем матрицу
  const transform = new DOMMatrixReadOnly(style.transform)

  const translateX = transform.m41
  const translateY = transform.m42

  startX = e.pageX - translateX
  startY = e.pageY - translateY
})

document.body.addEventListener('mouseup', () => {
  dragging = false
})

        
        
          
        
      

А также немного обновим изменение положения. В этот раз можем объединить обновлённые координаты в одну запись translate, которую потом присвоим в качестве значения свойству transform.

        
          
          // ...document.body.addEventListener('mousemove', (e) => {  if (!dragging) return  const x = e.pageX - startX  const y = e.pageY - startY  element.style.transform = `translate(${x}px, ${y}px)`})
          // ...

document.body.addEventListener('mousemove', (e) => {
  if (!dragging) return

  const x = e.pageX - startX
  const y = e.pageY - startY

  element.style.transform = `translate(${x}px, ${y}px)`
})

        
        
          
        
      

В итоге получим такой же драг-н-дроп, но работающий на transform.

Открыть демо в новой вкладке

Но мы можем ещё лучше 😎

Изменение кастомных свойств CSS

Скопировано

Сейчас код рабочий, но его трудно читать. Как минимум потому, что надо знать, как работает матрица преобразований и DOMMatrixReadOnly.

Мы же можем не менять значение transform вовсе, а вместо этого менять значение CSS-переменных, чтобы обновлять положение элемента!

Первым делом определяем кастомные свойства CSS в стилях элемента. В переменной --x будем держать значение координаты по горизонтали, в переменной --y — по вертикали. Укажем transform, значением которого передадим translate с указанными переменными. В итоге нам не придётся менять сам transform, мы сможем ограничиться лишь изменением значений переменных --x и --y.

        
          
          .element {  width: 50px;  height: 50px;  background: black;  position: absolute;  --x: 0px;  --y: 0px;  transform: translate(var(--x), var(--y));}
          .element {
  width: 50px;
  height: 50px;
  background: black;
  position: absolute;

  --x: 0px;
  --y: 0px;

  transform: translate(var(--x), var(--y));
}

        
        
          
        
      

Теперь подправим скрипт, чтобы сперва считать значение этих переменных:

        
          
          // ...element.addEventListener('mousedown', (e) => {  dragging = true  // Получаем стиль элемента  const style = window.getComputedStyle(element)  // Считываем значение каждой переменной через getPropertyValue  const translateX = parseInt(style.getPropertyValue('--x'))  const translateY = parseInt(style.getPropertyValue('--y'))  // Остаётся по-старому :–)  startX = e.pageX - translateX  startY = e.pageY - translateY})
          // ...

element.addEventListener('mousedown', (e) => {
  dragging = true

  // Получаем стиль элемента
  const style = window.getComputedStyle(element)

  // Считываем значение каждой переменной через getPropertyValue
  const translateX = parseInt(style.getPropertyValue('--x'))
  const translateY = parseInt(style.getPropertyValue('--y'))

  // Остаётся по-старому :–)
  startX = e.pageX - translateX
  startY = e.pageY - translateY
})

        
        
          
        
      

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

        
          
          // ...document.body.addEventListener('mousemove', (e) => {  if (!dragging) return  element.style.setProperty('--x', `${e.pageX - startX}px`)  element.style.setProperty('--y', `${e.pageY - startY}px`)})
          // ...

document.body.addEventListener('mousemove', (e) => {
  if (!dragging) return
  element.style.setProperty('--x', `${e.pageX - startX}px`)
  element.style.setProperty('--y', `${e.pageY - startY}px`)
})

        
        
          
        
      

В результате получаем такой же драг-н-дроп!

Открыть демо в новой вкладке

На практике

Скопировано

Саша Беспоясов советует

Скопировано

🛠 Всегда старайтесь стилизовать элементы с помощью CSS-классов. Если анимацию можно сделать с помощью смены классов, описывайте стили в них.

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

В примере ниже используем Прокрутчик, чтобы таскать блоки мышью и крутить их с инерцией:

Открыть демо в новой вкладке

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

🛠 Старайтесь анимировать свойства transform и opacity, чтобы сделать сайт или приложение более отзывчивыми.