Create floating windows in your Vue application

Vue 3 Floating Windows

Breaking down Floating Window component from my personal project.

Problem

For one of the projects we were in need of window management system that is reliable and easy to use. User should have an option to fill in the form and see the data all over the screen at the same time. Session could be suspended by minimizing the window. Let's see a working example and then breakdown how this component works.

Example

Prerequirement

Our example will be build with following:

The code

Let's start with simplified code of the template

<!-- <FloatingWindow> -->
<template>
  <!-- Use v-show to make sure width and height will be calculated -->
  <div
    v-show="modelValue"
    class="fixed rounded-3xl shadow-xl backdrop-blur-sm"
    :style="`top: ${windowPosition.top}px; left: ${windowPosition.left}px`"
  >
    <!-- Register top bar as drag handle -->
    <div
      ref="windowHandle"
      @mousedown.prevent="startDragging"
      @touches.prevent="startDragging"
      class="h-4 w-full gap-1"
    ></div>
    <div class="flex-1 overflow-auto">
      <!-- Pass content to slot -->
      <slot></slot>
    </div>
  </div>
</template>

Since we want windowHandle not to overflow the screen it will be our calculation point.

Now it's time for our start drag logic:

<!-- <FloatingWindow> -->
<script setup lang="ts">
// Define v-model for FloatingWindow component
const emit = defineEmits(["update:modelValue"]);
const props = defineProps({
  modelValue: Boolean,
});

// Reference to window handle
const windowHandle = ref(null);

// Define tempPosition as reference to drag
const tempPosition = ref({
  x: 0,
  y: 0,
});

// Define postion of the window
const windowPosition = ref({
  top: 0,
  left: 0,
});

// Check drag state
const dragging = ref(false);

// Handle start drag
const startDragging = (event) => {
  dragging.value = true;
  tempPosition.value.x = event.clientX - windowPosition.value.left;
  tempPosition.value.y = event.clientY - windowPosition.value.top;

  // Create window event listeners for drag event and stop drag
  window.addEventListener("mousemove", dragElement);
  window.addEventListener("mouseup", stopDragging);
};
</script>

Now we need to define how to calculate position of the Floating Window while mouse is dragging the element:

<!-- <FloatingWindow> -->
<script setup lang="ts">
// ...

// Handle drag action
const dragElement = (event) => {
  event.preventDefault();
  if (dragging.value) {
    // Get position of the window handle
    const windowRect = windowHandle.value.getBoundingClientRect();
    const clientX = event.clientX;
    const clientY = event.clientY;

    // Calculate the new position
    let left = clientX - tempPosition.value.x;
    let top = clientY - tempPosition.value.y;

    // Check if the target is in bounds of the body
    const bodyRect = document.body.getBoundingClientRect();

    // Check if element is touching it's boundaries
    if (left < bodyRect.left) {
      left = bodyRect.left;
    } else if (left + windowRect.width > bodyRect.right) {
      left = bodyRect.right - windowRect.width;
    }

    if (top < bodyRect.top) {
      top = bodyRect.top;
    } else if (top + windowRect.height > bodyRect.bottom) {
      top = bodyRect.bottom - windowRect.height;
    }

    // Update position of the window
    windowPosition.value.left = left;
    windowPosition.value.top = top;
  }
};
</script>

And cleanup once the drag event is finished:

<!-- <FloatingWindow> -->
<script setup lang="ts">
// ...

// Remove event listeners on stop drag
const stopDragging = () => {
  dragging.value = false;
  window.removeEventListener("mousemove", dragElement);
  window.removeEventListener("mouseup", stopDragging);
};
</script>

Example in the beginning fetures extra functionality like minimizing. This could be done with state management library like Pinia and additional component that stores minimized windows.

Making the window sticky

Code presented above ensures that window will float while scrolling throught the page. To make it sticks to the page is simple as substracting the scrollTop and scrollLeft values from the windowPosition coordinates.

<script setup lang="ts">
// ...

const stickyPosition = computed(() => {
  return {
    top: windowPosition.value.top - scrollTop.value,
    left: windowPosition.value.left - scrollLeft.value,
  };
});
</script>

Going wild

If you really wish to step up your Floating Window game, consider detaching the window. In next story we will dive in how this could be achieved and what are the potential use cases.

Conclusion

This window management system could have a lot of benefits in financial applications or when user has to be focused on large amount of data like listing tables.