Briefly
Elements on the page can be positioned not only using styles but also using JavaScript. In this article, we will look at situations when this is justified and how to use such positioning.
Before we move on to positioning, let's define why we need another way to arrange elements when we already have CSS.
When to use styles
Use styles for positioning whenever possible.
CSS is a tool specifically designed for styling documents.
- It works, on average, faster.
- It won't confuse other developers who will read your code.
- It separates areas of responsibility between scripts and styles.
When to use scripts
Use scripts for positioning when styles are not enough.
CSS is limited in response to user actions on the screen. It has things like @keyframes
, transition
, :hover
, :active
, :focus
, etc., but that's not always enough.
Sometimes we need complex transformations to occur in response to user actions on the page, or for users to control animations on the page themselves.
Such cases are not just styling a document but rather a blend of styling and programming logic. To solve such tasks, we need both styling tools (CSS) and tools for programming logic (JavaScript).
How to change positioning with scripts
Changing the position of elements on the page (like any styles of elements) can be done in several ways.
Change classes
Suppose we want to move an element when clicked to another place. To solve such a task, we can use the method of changing the class of the element.
Let's define the CSS classes:
.element { /* Styles for the element itself */}.element-initial { /* Styles that define the initial position of the element on the page */ transform: translateX(0px);}.element-final { /* Styles that define the final position */ transform: translateX(50px);}
.element { /* Styles for the element itself */ } .element-initial { /* Styles that define the initial position of the element on the page */ transform: translateX(0px); } .element-final { /* Styles that define the final position */ transform: translateX(50px); }
The element initially has the classes element element
, which set its styles and its initial position.
Now, in response to user action (for example, in response to a click), we will change the class of the element responsible for the position. We will use the class
method on the element to add the class if it's not on the element and remove it if the class is present:
// Handle the click event on the elementelement.addEventListener('click', () => { element.classList.toggle('element-final') element.classList.toggle('element-initial')})
// Handle the click event on the element element.addEventListener('click', () => { element.classList.toggle('element-final') element.classList.toggle('element-initial') })
Thus, we will get an element that changes its position when clicked.
This method of changing element styles with scripts is the simplest and cleanest — all styles remain described within CSS. However, it is not always suitable.
Such a method can be used when we know in advance where and from where we want to move the element, but do not know the moment when we will need it.
Change style
The second way to change the position of an element is to change the style
attribute using JavaScript.
When working with style
, one should remember that this attribute has high specificity, causing it to override the element's main styles. It should be used with caution.
It will suit cases when we want to reflect changes on the element instantly, even if we do not know what and when will change. For example, if we want to move the element with the mouse on the screen, we may need to change its style
.
To change the position through style
, we can use different properties.
Changing margin
or top / left / right / bottom
The first thing that comes to mind is changing the corresponding CSS properties like margin
or left
.
Let's create an element with the class element
:
.element { width: 50px; height: 50px; background: black; position: absolute;}
.element { width: 50px; height: 50px; background: black; position: absolute; }
Now let’s try to write a drag-and-drop for the mouse. First, we will create a reference to this element to handle events on it. The dragging
variable will indicate the state of the element. If it's being dragged, the variable will be true. By default, it is false. The variables start
and start
will hold the coordinates of the point where the element was located when we started dragging it with the mouse.
In the mousedown
event, when the element is clicked, we mark dragging
as true — meaning the element has started to be dragged. We assign values for start
and start
with the cursor position through the event properties e
and e
. We subtract the element's offsets if any. The offsets subtraction is necessary so that the element "remembers" its last position; otherwise, we will always start dragging it from the beginning of the screen.
Next, we handle the mouse movement event on <body>
. We observe <body>
because we want the changes to work on the entire page, not just inside the element
. If the element is not being dragged, we do nothing. If it is being dragged, we calculate the new position by subtracting the initial position of the element from the position of the cursor. When we release the mouse, we mark dragging
as false.
// Create a referenceconst element = document.querySelector('.element')let dragging = false// Initial coordinateslet startX = 0let startY = 0// Event when dragging the elementelement.addEventListener('mousedown', (e) => { dragging = true startX = e.pageX - Number.parseInt(element.style.left || 0) startY = e.pageY - Number.parseInt(element.style.top || 0)})// Handle the mouse movement event on <body>document.body.addEventListener('mousemove', (e) => { // The element is not being dragged if (!dragging) return // The element is being dragged element.style.top = `${e.pageY - startY}px` element.style.left = `${e.pageX - startX}px`})// Release the mousedocument.body.addEventListener('mouseup', () => { dragging = false})
// Create a reference const element = document.querySelector('.element') let dragging = false // Initial coordinates let startX = 0 let startY = 0 // Event when dragging the element element.addEventListener('mousedown', (e) => { dragging = true startX = e.pageX - Number.parseInt(element.style.left || 0) startY = e.pageY - Number.parseInt(element.style.top || 0) }) // Handle the mouse movement event on <body> document.body.addEventListener('mousemove', (e) => { // The element is not being dragged if (!dragging) return // The element is being dragged element.style.top = `${e.pageY - startY}px` element.style.left = `${e.pageX - startX}px` }) // Release the mouse document.body.addEventListener('mouseup', () => { dragging = false })
Now we have a drag-and-drop like this:
This works but is not very efficient, as changes in these properties cause the browser to do a lot of unnecessary work.
We can do better.
Changing transform
Let's rewrite our drag-and-drop to now change the value of the transform
property.
The base code will remain the same, the styles and markup will not change at all. In the scripts, we will slightly change the definition of the element's position.
This time we cannot read the necessary values directly. Instead, we need to first compute the style of the element with window
, and then find the value of the transform
property. We could simply read the value of style
, but that wouldn’t help much. When reading normally, we would get something like matrix
. This is a matrix of affine transformations. It can be represented as matrix
, where:
scale
— scaling horizontally;X scale
— scaling vertically;Y skew
— skew horizontally;X skew
— skew vertically;Y translate
— translation horizontally;X translate
— translation vertically.Y
But even considering that we have all the necessary numbers, working with it is inconvenient — it’s just a string. Fortunately, we can use DOM
, which converts this matrix into a usable format. After that, we can use the properties that contain translate
and translate
values. Then, like before, we subtract translate
and translate
instead of top
and left
. Finally, we add the ability to release the element when the button is released.
// ...element.addEventListener('mousedown', (e) => { dragging = true const style = window.getComputedStyle(element) // Convert the matrix 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) // Convert the matrix 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 })
We will also slightly update the position change. This time we can combine the updated coordinates into one translate
record, which we will then assign as the value of the transform
property.
// ...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)` })
As a result, we will have a similar drag-and-drop but working on transform
.
But we can do even better 😎
Changing custom CSS properties
Now the code is functional, but it’s difficult to read. At least because you need to understand how the transformation matrix and DOM
work.
We can avoid changing the transform
value at all and instead change the values of CSS variables to update the element’s position!
First, we define custom CSS properties in the element's styles. In the variable -
, we will hold the value of the horizontal coordinate, and in the variable -
— the vertical one. We will set transform
to pass translate
with the specified variables. As a result, we won't need to change transform
itself; we can limit ourselves to just changing the values of the variables -
and -
.
.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)); }
Now let's adjust the script to first read the value of these variables:
// ...element.addEventListener('mousedown', (e) => { dragging = true // Get the style of the element const style = window.getComputedStyle(element) // Read the value of each variable using getPropertyValue const translateX = parseInt(style.getPropertyValue('--x')) const translateY = parseInt(style.getPropertyValue('--y')) // Remains the same as before :–) startX = e.pageX - translateX startY = e.pageY - translateY})
// ... element.addEventListener('mousedown', (e) => { dragging = true // Get the style of the element const style = window.getComputedStyle(element) // Read the value of each variable using getPropertyValue const translateX = parseInt(style.getPropertyValue('--x')) const translateY = parseInt(style.getPropertyValue('--y')) // Remains the same as before :–) startX = e.pageX - translateX startY = e.pageY - translateY })
Now let’s modify the style update. Notice how concise the notation has become. We simply specify what value each variable should take.
// ...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`) })
As a result, we get the same drag-and-drop functionality!
In practice
Advice 1
🛠 Always try to style elements using CSS classes. If the animation can be done by changing classes, describe the styles in them.
🛠 Changing element styles directly can be useful when you are writing an animation that directly depends on user actions, and they cannot be predicted.
In the example below, we use Scroller to drag blocks with the mouse and scroll them with inertia:
We position elements using scripts because we do not know when and how the user wants to scroll the feed of blocks.
🛠 Try to animate properties transform
and opacity
, to make the site or application more responsive.