DEV Community

Cover image for Draggable & Resizable Window on your Website
Markus Wedler
Markus Wedler

Posted on • Edited on

Draggable & Resizable Window on your Website

Create a draggable and resizable window that looks and feels like those in your desktop OS.
The source code is located at the end of the post.

❔How it works

Now, let's just briefly look at HTML and then I'll explain the JS part.

HTML

<div class="window">
  <div class="resizer corner tl"></div>
  <div class="resizer corner tr"></div>
  <div class="resizer corner bl"></div>
  <div class="resizer corner br"></div>
  <div class="resizer t"></div>
  <div class="resizer b"></div>
  <div class="resizer l"></div>
  <div class="resizer r"></div>
  <div class="body">
    <div class="topbar">
      <div class="btns">
        <div></div>
        <div></div>
        <div></div>
      </div>
    </div>
    <!-- your content here -->
  </div>
</div>

Here we have 4 resizers at the corners and 4 at the sides of the window. We'll be able to drag the window holding its topbar.

JS

Let's declare variables-selectors of the window and topbar.

const xwindow = document.querySelector(".window")
const topbar = document.querySelector(".topbar")

Dragging

We'll be able to drag the window while holding the mouse button and moving it. The mousemove listener will only be added when mousedown is triggered. So we also need mouseup to remove mousemove when releasing the mouse button.
Let's now add mousedown listener to the topbar and create a mousedown() function.

//topbar can be whatever you want to hold while dragging
topbar.addEventListener("mousedown", mousedown)
function mousedown(){
  window.addEventListener("mousemove", mousemove)
  window.addEventListener("mouseup", mouseup)
  function mousemove(){}
  function mouseup(){
    window.removeEventListener("mousemove", mousemove)
    window.removeEventListener("mouseup", mouseup)
  }
}

Ok, so now we want JS to track out cursor's position and move the windows as we move the cursor. To do so we need e (or event) which besides other stuff stores information about cursor position. Let's pass it as a function argument.

function mousedown(e){
  ...
  function mousemove(e){}
  ...
}

To change the position of the window we can add the difference of the new and previous position of the cursor and add this difference to the left and top values of the window (our window is absolutely positioned). Let's get previous values of the cursor's coordinates. Before we move the cursor, these values are just the current values.

function mousedown(e){
  ...
  let prevX = e.clientX
  let prevY = e.clientY
  function mousemove(e){}
  ...
}

Now let's find the difference between new and previous positions.

...
let prevX = e.clientX
let prevY = e.clientY
function mousemove(e){
  let newX = e.clientX - prevX
  let newY = e.clientY - prevY
}
...

JavaScript method getBoundingClientRect() returns a DOMRect object providing information about the size of an element and its position relative to the viewport. With it we can get the values of top and left properties and set new values.

function mousemove(e){
  let newX = e.clientX - prevX
  let newY = e.clientY - prevY
  const rect = xwindow.getBoundingClientRect()
  xwindow.style.left = rect.left + newX + "px"
  xwindow.style.top = rect.top + newY + "px"
  prevX = e.clientX
  prevY = e.clientY
}

Congrats! Now we can drag our window.

Resizing

Let's get all the resizers.

const resizers = document.querySelectorAll(".resizer")

As with the topbar, we want to resize the window only when holding the resizer. So let's do the similar thing but in for loop to apply for every resizer.

for (let resizer of resizers){
  resizer.addEventListener("mousedown", mousedown)
  function mousedown(e){
    window.addEventListener("mousemove", mousemove)
    window.addEventListener("mouseup", mouseup)
    let prevX = e.clientX
    let prevY = e.clientY
    function mousemove(e){
      const rect = xwindow.getBoundingClientRect()
      prevX = e.clientX
      prevY = e.clientY
    }
    function mouseup(){
      window.removeEventListener("mousemove", mousemove)
      window.removeEventListener("mouseup", mouseup)
    }
  }
}

Now we need to tell JS which resizer we're holding. We can easily check it by their class. For example, tl means "top-left". To check if current resizer has such class at first we need to declare currentResiser and assign it to e.target (the element we're holding).

...
let prevY = e.clientY
let currentResizer = e.target
...

To check if there's some class in classes list of the resizer, we use currentResizer.classList.contains("tl") in if statement. If yes, we need to calculate new size to the windows. Also if it's top, left or top-left resizer, we also have to change top and left.

...
function mousemove(e){
  const rect = xwindow.getBoundingClientRect()
  if(currentResizer.classList.contains("br")){
    xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
    xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
  }
  else if(currentResizer.classList.contains("bl")){
    xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
    xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
    xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
  }
  else if(currentResizer.classList.contains("tr")){
    xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
    xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
    xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
  }
  else if(currentResizer.classList.contains("tl")){
    xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
    xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
    xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
    xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
  }
  else if(currentResizer.classList.contains("t")){
    xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
    xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
  }
  else if(currentResizer.classList.contains("b")){
    xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
  }
  else if(currentResizer.classList.contains("l")){
    xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
    xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
  }
  else if(currentResizer.classList.contains("r")){
    xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
  }
  prevX = e.clientX
  prevY = e.clientY
}
...

Voila! Now we also can resize it.

The Full Code

HTML

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <link rel="stylesheet" href="style.css">
  <script defer src="index.js"></script>
</head>
<body>

  <div class="window">
    <div class="resizer corner tl"></div>
    <div class="resizer corner tr"></div>
    <div class="resizer corner bl"></div>
    <div class="resizer corner br"></div>
    <div class="resizer t"></div>
    <div class="resizer b"></div>
    <div class="resizer l"></div>
    <div class="resizer r"></div>

    <div class="body">
      <div class="topbar">
        <div class="btns">
          <div></div>
          <div></div>
          <div></div>
        </div>
      </div>

      <!-- your content here -->
    </div>
  </div>

</body>
</html>
Enter fullscreen mode Exit fullscreen mode

SCSS

*{
  margin: 0;
  box-sizing: border-box;
}


.window{
  width: 600px;
  height: 400px;
  min-width: 300px;
  min-height: 200px;
  position: absolute;

  .resizer{
    position: absolute;
    z-index: 1;
    width: 22px;
    height: 22px;
    background: #41a94c80; //delete
    &.corner{
      z-index: 2;
      &.tl{ cursor: nw-resize; top: -5px; left: -5px; }
      &.tr{ cursor: ne-resize; top: -5px; right: -5px; }
      &.bl{ cursor: sw-resize; bottom: -5px; left: -5px; }
      &.br{ cursor: se-resize; bottom: -5px; right: -5px; }
    }
    &.t, &.b{
      width: 100%;
      height: 14px;
    }
    &.l, &.r{
      width: 14px;
      height: 100%;
    }
    &.t{ cursor: n-resize; top: -5px; }
    &.b{ cursor: s-resize; bottom: -5px; }
    &.l{ cursor: w-resize; left: -5px; }
    &.r{ cursor: e-resize; right: -5px; }
  }

  .body{
    border-radius: 15px;
    overflow: hidden;
    height: 100%;
    background: #f4f4f4;
    .topbar{
      width: 100%;
      height: 60px;
      background: #8c8c8c;
      display: flex;
      align-items: center;
      padding: 0 30px;
      .btns{
        display: flex;
        gap: 9px;
        div{
          height: 14px;
          width: 14px;
          border-radius: 50%;
          &:nth-child(1){ background:#FF5F58; }
          &:nth-child(2){ background:#FFBE2F; }
          &:nth-child(3){ background:#2AC940; }
        }
      }
    }
  }

}
Enter fullscreen mode Exit fullscreen mode

JS

const xwindow = document.querySelector(".window")

const topbar = document.querySelector(".topbar")
topbar.addEventListener("mousedown", mousedown)

function mousedown(e){
  window.addEventListener("mousemove", mousemove)
  window.addEventListener("mouseup", mouseup)
  let prevX = e.clientX
  let prevY = e.clientY
  function mousemove(e){
    let newX = e.clientX - prevX
    let newY = e.clientY - prevY
    const rect = xwindow.getBoundingClientRect()
    xwindow.style.left = rect.left + newX + "px"
    xwindow.style.top = rect.top + newY + "px"
    prevX = e.clientX
    prevY = e.clientY
  }
  function mouseup(){
    window.removeEventListener("mousemove", mousemove)
    window.removeEventListener("mouseup", mouseup)
  }
}



const resizers = document.querySelectorAll(".resizer")

for (let resizer of resizers){
  resizer.addEventListener("mousedown", mousedown)
  function mousedown(e){
    let currentResizer = e.target
    let prevX = e.clientX
    let prevY = e.clientY
    window.addEventListener("mousemove", mousemove)
    window.addEventListener("mouseup", mouseup)
    function mousemove(e){
      const rect = xwindow.getBoundingClientRect()
      if(currentResizer.classList.contains("br")){
        xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
        xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
      }
      else if(currentResizer.classList.contains("bl")){
        xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
        xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
        xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
      }
      else if(currentResizer.classList.contains("tr")){
        xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
        xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
        xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
      }
      else if(currentResizer.classList.contains("tl")){
        xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
        xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
        xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
        xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
      }
      else if(currentResizer.classList.contains("t")){
        xwindow.style.height = rect.height + (prevY - e.clientY) + "px"
        xwindow.style.top = rect.top + (e.clientY - prevY) + "px"
      }
      else if(currentResizer.classList.contains("b")){
        xwindow.style.height = rect.height + (e.clientY - prevY) + "px"
      }
      else if(currentResizer.classList.contains("l")){
        xwindow.style.width = rect.width + (prevX - e.clientX) + "px"
        xwindow.style.left = rect.left + (e.clientX - prevX) + "px"
      }
      else if(currentResizer.classList.contains("r")){
        xwindow.style.width = rect.width + (e.clientX - prevX) + "px"
      }
      prevX = e.clientX
      prevY = e.clientY
    }
    function mouseup(){
      window.removeEventListener("mousemove", mousemove)
      window.removeEventListener("mouseup", mouseup)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)