Post

Building a Draggable Window Manager in Vanilla JavaScript

Building a Draggable Window Manager in Vanilla JavaScript

Desktop operating systems provide windowing interfaces where applications exist in movable, resizable containers. Recreating this paradigm in the browser requires no frameworks—vanilla JavaScript combined with CSS Flexbox produces a functional window manager in under 150 lines of code.

This post examines the implementation of draggable windows with title bars, window chrome buttons, and dynamic window creation.

Architecture Overview

The window manager comprises three components:

graph TD
    subgraph "Window Structure"
        W[window] --> TB[window-title-bar]
        W --> WC[window-content]
        TB --> T[window-title]
        TB --> BTN[window-title-bar-buttons]
        BTN --> H[hide]
        BTN --> E[expand]
        BTN --> C[close]
    end

Each window is a flex container with two children: the title bar and the content area. The title bar itself is a flex container arranging the title text and button group.

CSS Foundation

Window Container

The window element uses column-direction flex layout to stack the title bar above the content:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
:root {
  --window-button-size: 20px;
}

.window {
  display: flex;
  flex-wrap: wrap;
  flex-direction: column;
  justify-content: center;
  width: 400px;
  position: absolute;
  min-height: 150px;
  border-color: black;
  border-style: solid;
}

Absolute positioning enables free placement anywhere on screen. The min-height prevents collapse when content is minimal.

Title Bar Layout

The title bar demonstrates flexbox’s power for horizontal layouts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.window-title-bar {
  background: skyblue;
  align-items: center;
  display: flex;
  margin: 0;
  padding: 0;
  cursor: move;
  user-select: none;
}

p.window-title {
  margin-left: 15px;
  flex-direction: row;
  margin: 0;
  padding: 0;
  flex-grow: 1;
  z-index: 1;
  user-select: none;
}

The flex-grow: 1 on the title causes it to expand and fill available space, pushing the button group to the right edge. The user-select: none property prevents text highlighting during drag operations—without this, dragging a window would select the title text.

The cursor: move provides visual feedback that the title bar is draggable.

Window Chrome Buttons

The minimize, maximize, and close buttons use flex for centering icons:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.window-title-bar-buttons {
  display: flex;
  flex-direction: row;
  align-items: center;
  align-self: center;
}

.window-button-close,
.window-button-expand,
.window-button-hide {
  display: flex;
  font-family: fontawesome;
  border-color: black;
  border-style: solid;
  border-width: 1px;
  padding: 1px;
  margin: 2px;
  width: var(--window-button-size);
  height: var(--window-button-size);
  align-items: center;
  justify-content: center;
  align-self: center;
}

CSS custom properties (--window-button-size) enable consistent sizing across all buttons with a single value change.

Content Area

The content area expands to fill remaining window height:

1
2
3
4
.window-content {
  background: lightgrey;
  flex-grow: 1;
}

Drag Implementation

The drag system follows a standard pattern: capture initial mouse position on mousedown, calculate delta on mousemove, and clean up on mouseup.

Event Binding

1
2
3
4
5
6
7
function dragWindow(windowElmnt) {
  var x0 = 0,
      y0 = 0,
      x1 = 0,
      y1 = 0;

  windowElmnt.onmousedown = dragMouseDown;

Four variables track positions: x0 and y0 store the previous mouse position, while x1 and y1 store the calculated delta.

Mouse Down Handler

1
2
3
4
5
6
7
  function dragMouseDown(e) {
    e = e || window.event;
    x0 = e.clientX;
    y0 = e.clientY;
    document.onmouseup = closeDragWindow;
    document.onmousemove = windowDrag;
  }

The initial click position is captured, and event listeners are attached to the document (not the window element). Attaching to the document ensures drag continues even if the cursor moves faster than the element can follow.

Mouse Move Handler

1
2
3
4
5
6
7
8
9
  function windowDrag(e) {
    e = e || window.event;
    x1 = x0 - e.clientX;
    y1 = y0 - e.clientY;
    x0 = e.clientX;
    y0 = e.clientY;
    windowElmnt.style.top = (windowElmnt.offsetTop - y1) + "px";
    windowElmnt.style.left = (windowElmnt.offsetLeft - x1) + "px";
  }

The delta calculation (x0 - e.clientX) determines how far the mouse moved since the last event. This delta is subtracted from the element’s current position. The previous position is then updated for the next calculation.

Cleanup

1
2
3
4
5
  function closeDragWindow() {
    document.onmouseup = null;
    document.onmousemove = null;
  }
}

Removing event listeners prevents memory leaks and stops drag behavior when the mouse button is released.

Dynamic Window Creation

Windows are created programmatically rather than defined in static HTML:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function createWindow(id) {
  // Window container
  var newWindow = document.createElement("div");
  newWindow.className = "window";
  newWindow.id = id;

  // Title bar
  var titleBar = document.createElement("div");
  titleBar.className = "window-title-bar";

  // Title text
  var title = document.createElement('p');
  title.className = "window-title";
  title.appendChild(document.createTextNode("title"));

  // Button container
  var titleBarButtons = document.createElement("div");
  titleBarButtons.className = "window-title-bar-buttons";

  // Hide button
  var windowButtonHide = document.createElement("div");
  windowButtonHide.className = "window-button-hide";
  var faMinusSquare = document.createElement('span');
  faMinusSquare.className = "far fa-minus-square";
  windowButtonHide.appendChild(faMinusSquare);

  // Expand button
  var windowButtonExpand = document.createElement("div");
  windowButtonExpand.className = "window-button-expand";
  var faSquare = document.createElement('span');
  faSquare.className = "far fa-square";
  windowButtonExpand.appendChild(faSquare);

  // Close button
  var windowButtonClose = document.createElement("div");
  windowButtonClose.className = "window-button-close";
  var faTimesCircle = document.createElement('span');
  faTimesCircle.className = "far fa-times-circle";
  windowButtonClose.appendChild(faTimesCircle);

  // Assemble button group
  titleBarButtons.appendChild(windowButtonHide);
  titleBarButtons.appendChild(windowButtonExpand);
  titleBarButtons.appendChild(windowButtonClose);

  // Content area
  var windowContent = document.createElement("div");
  windowContent.className = "window-content";
  windowContent.appendChild(document.createTextNode("content"));

  // Assemble window
  titleBar.appendChild(title);
  titleBar.appendChild(titleBarButtons);
  newWindow.appendChild(titleBar);
  newWindow.appendChild(windowContent);

  // Add to DOM and enable dragging
  document.body.appendChild(newWindow);
  dragWindow(newWindow);
}

FontAwesome icons provide the button graphics. The function concludes by calling dragWindow() to enable drag behavior on the newly created window.

Window Operations

Close Window

The close operation hides the window via CSS class toggle:

1
2
3
function closeWindow(e) {
  e.classList.add("close-window");
}
1
2
3
.close-window {
  display: none;
}

This approach preserves the window in the DOM for potential restoration, unlike removeChild() which destroys the element entirely.

Stubbed Operations

The minimize and maximize operations require additional state tracking:

1
2
3
4
5
6
7
function hideWindow() {
  // Minimize to taskbar or dock
}

function fullScreen() {
  // Expand to fill viewport
}

A complete implementation would store original dimensions before maximizing and restore them when toggling back to windowed mode.

Enhancements

Several improvements would bring this closer to a production windowing system:

Z-Index Management

Clicking a window should bring it to the front:

1
2
3
4
5
6
var zIndexCounter = 1;

function bringToFront(windowElmnt) {
  zIndexCounter++;
  windowElmnt.style.zIndex = zIndexCounter;
}

Call bringToFront() in the mousedown handler.

Edge Snapping

Windows could snap to screen edges when dragged nearby:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function windowDrag(e) {
  // ... existing delta calculation ...

  var snapThreshold = 20;
  var newLeft = windowElmnt.offsetLeft - x1;
  var newTop = windowElmnt.offsetTop - y1;

  // Snap to left edge
  if (newLeft < snapThreshold) newLeft = 0;

  // Snap to top edge
  if (newTop < snapThreshold) newTop = 0;

  windowElmnt.style.top = newTop + "px";
  windowElmnt.style.left = newLeft + "px";
}

Window Resizing

Resize handles at window edges and corners would enable dimension adjustment. This requires additional mouse tracking for the resize operation and cursor changes (cursor: ew-resize, cursor: ns-resize, cursor: nwse-resize) to indicate resize affordance.

Browser Compatibility

The implementation uses standard DOM APIs and CSS properties supported across modern browsers:

FeatureSupport
FlexboxIE 11+, all modern browsers
CSS Custom PropertiesIE excluded, all modern browsers
user-selectPrefixed in older browsers
clientX/clientYUniversal support

For IE 11 compatibility, replace CSS custom properties with static values and add -ms-user-select: none.

Complete Source

The full implementation is available on CodePen.

Conclusion

A functional window manager emerges from combining CSS Flexbox layout with JavaScript mouse event handling. The title bar’s flex-grow property elegantly solves the classic problem of positioning elements at opposite ends of a container. The drag implementation demonstrates proper event delegation by attaching move and up handlers to the document rather than the dragged element.

The architecture supports extension: window state management, resize operations, and taskbar integration can layer onto this foundation without restructuring the core drag and create logic.

This post is licensed under CC BY 4.0 by the author.