A computer arrow moves windows with images

Positioning elements using JavaScript

CSS is great for positioning elements, but sometimes it's not enough. Learn to choose when to use CSS and when to use JavaScript.

Time to read: 11 min

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-initial, 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 classList.toggle() 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.

Open demo in the new window

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 / top / right / bottom.

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 startX and startY 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 startX and startY with the cursor position through the event properties e.pageX and e.pageY. 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:

Open demo in the new window

This works but is not very efficient, as changes in these properties cause the browser to do a lot of unnecessary work.

How the browser renders pages

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.getComputedStyle(), and then find the value of the transform property. We could simply read the value of style.transform, but that wouldn’t help much. When reading normally, we would get something like matrix(1, 0, 0, 1, 27, 15). This is a matrix of affine transformations. It can be represented as matrix(scaleX, skewY, skewX, scaleY, translateX, translateY), where:

  • scaleX — scaling horizontally;
  • scaleY — scaling vertically;
  • skewX — skew horizontally;
  • skewY — skew vertically;
  • translateX — translation horizontally;
  • translateY — translation vertically.

But even considering that we have all the necessary numbers, working with it is inconvenient — it’s just a string. Fortunately, we can use DOMMatrixReadOnly, which converts this matrix into a usable format. After that, we can use the properties that contain translateX and translateY values. Then, like before, we subtract translateX and translateY 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.

Open demo in the new window

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 DOMMatrixReadOnly 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 --x, we will hold the value of the horizontal coordinate, and in the variable --y — 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 --x and --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));
}

        
        
          
        
      

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!

Open demo in the new window

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:

Open demo in the new window

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.