skip to main content

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

js

Кратко 🔗

Элементы на странице можно позиционировать не только с помощью стилей, но и с помощью 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, которые задают его стили, а также его начальное положение.

Теперь в ответ на действие пользователя (например, в ответ на клик), поменяем класс элемента, отвечающий за положение:

// Слушаем событие клика на элементе:
element.addEventListener("click", () => {
// При клике добавляем класс element-final:
element.classList.add("element-final")

// Чистим лишние старые классы, чтобы не мусорить:
element.classList.remove("element-initial")
})

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

See the Pen doka-positioning-via-classlist by Alexander Bespoyasov (@bespoyasov) on CodePen.

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

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

Изменять style 🔗

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

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

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

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

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

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

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

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

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

// Сперва создадим ссылку на этот элемент,
// чтобы слушать события на нём:
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
})

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

See the Pen doka-positioning-via-style by Alexander Bespoyasov (@bespoyasov) on CodePen.

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

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

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

Изменение 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);
//
// Это матрица афинных преобразований.
// Её можно представить в виде:
// martix(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("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.

See the Pen doka-positioning-via-style-transform by Alexander Bespoyasov (@bespoyasov) on CodePen.

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

Изменение CSS custom properties 🔗

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

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

Первым делом используем CSS custom properties в стилях элемента:

.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
})

А теперь изменим обновление стилей:

// ...

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

// Обратите внимание, насколько лаконичной стала запись.
// Мы всего лишь указываем, какое значение должна
// принять каждая из переменных:
element.style.setProperty("--x", `${e.pageX - startX}px`)
element.style.setProperty("--y", `${e.pageY - startY}px`)
})

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

See the Pen doka-positioning-via-css-custom-properties by Alexander Bespoyasov (@bespoyasov) on CodePen.

В работе 🔗

По возможности всегда старайтесь стилизовать элементы с помощью CSS-классов.

Если анимацию можно сделать с помощью смены классов, описывайте стили в них.

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

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

See the Pen doka-scroller-example by Alexander Bespoyasov (@bespoyasov) on CodePen.

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

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