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

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

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

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

Кратко

Скопировано

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

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

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

Скопировано

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

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

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

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

Скопировано

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

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

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

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

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

Скопировано

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

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

Скопировано

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

Определим 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 с помощью JS.

При работе со 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;
}

        
        
          
        
      

Теперь попробуем написать драг-н-дроп для мыши.

        
          
          // Сперва создадим ссылку на этот элемент,// чтобы обрабатывать события на нём:const element = document.querySelector('.element')// Переменная dragging будет отвечать за состояние элемента.// Если его тащат, то переменная будет со значением true.// По умолчанию она false.let dragging = false// В переменных startX и startY мы будем держать координаты точки,// в которой находился элемент, когда мы начали его тащить мышью.let startX = 0let startY = 0// При событии mousedown (когда на элемент нажимают мышью)// мы отмечаем dragging как true — значит, элемент начали тащить.element.addEventListener('mousedown', (e) => {  dragging = true  // В значения для startX и startY мы помещаем положение курсора  // через свойства события e.pageX и e.pageY.  startX = e.pageX - Number.parseInt(element.style.left || 0)  startY = e.pageY - Number.parseInt(element.style.top || 0)  // Из положения курсора мы вычитаем отступы элемента, если они есть.  // Вычитание отступов нам нужно, чтобы элемент «запоминал»  // своё последнее положение, иначе мы всегда будем начинать тащить его  // от начала экрана.})// Далее мы обрабатываем событие перемещения мыши по body.// Мы наблюдаем именно за body, потому что хотим,// чтобы изменения работали на всей странице,// а не только внутри элемента element.document.body.addEventListener('mousemove', (e) => {  // Если элемент не тащат, то ничего не делаем.  if (!dragging) return  // Если тащат, то высчитываем новое положение,  // вычитая начальное положение элемента из положения курсора.  element.style.top = `${e.pageY - startY}px`  element.style.left = `${e.pageX - startX}px`})// Когда мы отпускаем мышь, мы отмечаем dragging как false.document.body.addEventListener('mouseup', () => {  dragging = false})
          // Сперва создадим ссылку на этот элемент,
// чтобы обрабатывать события на нём:
const element = document.querySelector('.element')

// Переменная dragging будет отвечать за состояние элемента.
// Если его тащат, то переменная будет со значением true.
// По умолчанию она false.
let dragging = false

// В переменных startX и startY мы будем держать координаты точки,
// в которой находился элемент, когда мы начали его тащить мышью.
let startX = 0
let startY = 0

// При событии mousedown (когда на элемент нажимают мышью)
// мы отмечаем dragging как true — значит, элемент начали тащить.
element.addEventListener('mousedown', (e) => {
  dragging = true

  // В значения для startX и startY мы помещаем положение курсора
  // через свойства события e.pageX и e.pageY.
  startX = e.pageX - Number.parseInt(element.style.left || 0)
  startY = e.pageY - Number.parseInt(element.style.top || 0)

  // Из положения курсора мы вычитаем отступы элемента, если они есть.
  // Вычитание отступов нам нужно, чтобы элемент «запоминал»
  // своё последнее положение, иначе мы всегда будем начинать тащить его
  // от начала экрана.
})

// Далее мы обрабатываем событие перемещения мыши по body.
// Мы наблюдаем именно за body, потому что хотим,
// чтобы изменения работали на всей странице,
// а не только внутри элемента element.
document.body.addEventListener('mousemove', (e) => {
  // Если элемент не тащат, то ничего не делаем.
  if (!dragging) return

  // Если тащат, то высчитываем новое положение,
  // вычитая начальное положение элемента из положения курсора.
  element.style.top = `${e.pageY - startY}px`
  element.style.left = `${e.pageX - startX}px`
})

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

        
        
          
        
      

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

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

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

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

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

Изменение transform

Скопировано

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

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

        
          
          // ...element.addEventListener('mousedown', (e) => {  dragging = true  // В этот раз мы не сможем считать нужные нам значения напрямую.  // Вместо этого нам потребуется вначале вычислить стиль элемента  // через window.getComputedStyle(), а затем узнать значение  // свойства transform.  const style = window.getComputedStyle(element)  // Мы могли бы просто считать значение style.transform,  // но это бы нам не сильно помогло.  // При обычном считывании мы бы получили нечто вроде:  //     matrix(1, 0, 0, 1, 27, 15);  //  // Это матрица афинных преобразований.  // Её можно представить в виде:  //     matrix(scaleX, skewY, skewX, scaleY, translateX, translateY);  // где:  //     - scaleX — масштабирование по горизонтали,  //     - scaleY — масштабирование по вертикали,  //     - skewX — перекос по горизонтали,  //     - skewY — перекос по вертикали,  //     - translateX — смещение по горизонтали,  //     - translateY — смещение по вертикали.  //  // Но даже учитывая, что у нас есть все необходимые числа,  // работать с этим неудобно — это же просто строка.  //  // К счастью мы можем воспользоваться DOMMatrixReadOnly,  // который преобразует эту матрицу в удобную для использования:  const transform = new DOMMatrixReadOnly(style.transform)  // Теперь мы можем воспользоваться свойствами,  // которые содержат в себе значения translateX и translateY.  const translateX = transform.m41  const translateY = transform.m42  // Дальше — как раньше, только вычитаем не top и left,  // а translateX и translateY.  startX = e.pageX - translateX  startY = e.pageY - translateY})// добавляем возможность отпустить элемент при отжатии клавишиdocument.body.addEventListener("mouseup", () => {  dragging = false;});
          // ...

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

  // В этот раз мы не сможем считать нужные нам значения напрямую.
  // Вместо этого нам потребуется вначале вычислить стиль элемента
  // через window.getComputedStyle(), а затем узнать значение
  // свойства transform.
  const style = window.getComputedStyle(element)

  // Мы могли бы просто считать значение style.transform,
  // но это бы нам не сильно помогло.
  // При обычном считывании мы бы получили нечто вроде:
  //     matrix(1, 0, 0, 1, 27, 15);
  //
  // Это матрица афинных преобразований.
  // Её можно представить в виде:
  //     matrix(scaleX, skewY, skewX, scaleY, translateX, translateY);
  // где:
  //     - scaleX — масштабирование по горизонтали,
  //     - scaleY — масштабирование по вертикали,
  //     - skewX — перекос по горизонтали,
  //     - skewY — перекос по вертикали,
  //     - translateX — смещение по горизонтали,
  //     - translateY — смещение по вертикали.
  //
  // Но даже учитывая, что у нас есть все необходимые числа,
  // работать с этим неудобно — это же просто строка.
  //
  // К счастью мы можем воспользоваться DOMMatrixReadOnly,
  // который преобразует эту матрицу в удобную для использования:
  const transform = new DOMMatrixReadOnly(style.transform)

  // Теперь мы можем воспользоваться свойствами,
  // которые содержат в себе значения translateX и translateY.
  const translateX = transform.m41
  const translateY = transform.m42

  // Дальше — как раньше, только вычитаем не top и left,
  // а translateX и translateY.
  startX = e.pageX - translateX
  startY = e.pageY - translateY
})
// добавляем возможность отпустить элемент при отжатии клавиши
document.body.addEventListener("mouseup", () => {
  dragging = false;
});

        
        
          
        
      

А также немного обновим изменение положения:

        
          
          // ...document.body.addEventListener('mousemove', (e) => {  if (!dragging) return  const x = e.pageX - startX  const y = e.pageY - startY  // В этот раз мы можем объединить обновлённые координаты  // в одну запись translate, которую потом  // присвоим в качестве значения свойству transform.  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

  // В этот раз мы можем объединить обновлённые координаты
  // в одну запись translate, которую потом
  // присвоим в качестве значения свойству transform.
  element.style.transform = `translate(${x}px, ${y}px)`
})

        
        
          
        
      

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

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

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

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

Скопировано

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

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

Первым делом определяем кастомные свойства CSS в стилях элемента:

        
          
          .element {  width: 50px;  height: 50px;  background: black;  position: absolute;  /* В переменной --x мы будем держать    значение координаты по горизонтали;    в переменной --y — по вертикали. */  --x: 0px;  --y: 0px;  /* Укажем transform, значением которого    передадим translate с указанными переменными.    В итоге нам не придётся менять сам transform,    мы сможем ограничиться лишь изменением значений    переменных --x и --y. */  transform: translate(var(--x), var(--y));}
          .element {
  width: 50px;
  height: 50px;
  background: black;
  position: absolute;

  /* В переменной --x мы будем держать
    значение координаты по горизонтали;
    в переменной --y — по вертикали. */
  --x: 0px;
  --y: 0px;

  /* Укажем transform, значением которого
    передадим translate с указанными переменными.
    В итоге нам не придётся менять сам transform,
    мы сможем ограничиться лишь изменением значений
    переменных --x и --y. */
  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`)
})

        
        
          
        
      

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

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

На практике

Скопировано

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

Скопировано

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

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

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

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

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