npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

v-movable

v1.0.5

Published

A Vue 3 component that wraps lit-movable — drag/move any element, with grid snap, relative bounds, axis lock, and rich move state.

Downloads

709

Readme

v-movable npm version tests License: MIT

A Vue 3 component that makes any element movable — drag, snap to a grid, clamp to bounds, lock an axis, and read back rich move state. It wraps the lit-movable web component under the hood, so you get a battle-tested engine with a clean, idiomatic Vue API.

You only ever write <v-movable>. The custom element is registered for you — no Lit, no isCustomElement config, no extra setup in your app.

Live Demo

Installation

npm i v-movable

vue@^3 is a peer dependency. lit-movable is a regular dependency (installed automatically).

Register globally

// main.js
import { createApp } from 'vue';
import VMovable from 'v-movable';
import App from './App.vue';

createApp(App).use(VMovable).mount('#app');
// <v-movable> is now available everywhere

Or import locally

<script setup>
import { VMovable } from 'v-movable';
</script>

Basic usage

<template>
  <v-movable class="box" :left="40" :top="40">
    <span>drag me</span>
  </v-movable>
</template>

<style>
.box { width: 120px; height: 120px; background: #333; color: #fff; }
</style>

The component positions itself absolutely (display:block; position:absolute is applied to the host). Give it a size with your own class/style.

Two-way coordinates with v-model

<script setup>
import { ref } from 'vue';
const x = ref(50);
const y = ref(40);
</script>

<template>
  <v-movable v-model:left="x" v-model:top="y" class="box">
    <span>{{ Math.round(x) }}, {{ Math.round(y) }}</span>
  </v-movable>
</template>

During an active drag the engine owns the position; v-model updates are emitted to you but not pushed back into the element mid-gesture (this avoids feedback against the drag math). Between gestures, setting x / y repositions the element.

Props

| Prop | Type | Description | | --- | --- | --- | | left / top | Number | Initial / v-model coordinate (px). Set before boundsX/boundsY when both change in one update. | | grid | Number | Snap increment in px (default 1). | | boundsX / boundsY | String | Relative "min,max" offsets from the current left/top. "null" locks that axis. See the bounds model below. | | axis | "x" | "y" | Lock the orthogonal axis to the current position. | | disabled | Boolean | Disable dragging. | | shiftBehavior | Boolean | With open bounds, holding Shift constrains to the dominant axis. | | eventsOnly | Boolean | Fire events but do not reposition the target. | | targetSelector | String | CSS selector for the element that moves (default: the <v-movable> host itself). | | dragAfterDist | Number | Pointer travel (px) before a drag starts (default 0). |

Events

All events carry lit-movable's plain move-state object:

{ coords, startCoord, moveDist, totalDist, mouseCoord, clickOffset,
  posTop, posLeft, pctX, pctY, isMoving }

| Event | When | | --- | --- | | @movestart | Drag begins (after dragAfterDist is exceeded). | | @move | Continuously while moving. | | @moveend | Pointer released. | | @update:left / @update:top | v-model coordinate updates during @move. |

<v-movable @movestart="onStart" @move="onMove" @moveend="onEnd" />

Bounds model (read this)

boundsX / boundsY are deltas from the element's current position, not absolute style.left/top ranges:

absoluteMin = currentLeft + min
absoluteMax = currentLeft + max

So a knob already at left: 85 that must stay inside [0, 160]:

<!-- WRONG — parses as [85, 245] -->
<v-movable :left="85" boundsX="0,160" />

<!-- RIGHT — deltas from 85 → absolute [0,160] -->
<v-movable :left="85" boundsX="-85,75" />

Recipe for "stay inside [0, size]" while at (left, top):

const boundsX = `${-left},${size - left}`;
const boundsY = `${-top},${size - top}`;

Gotchas (inherited from lit-movable):

  1. Set left/top before boundsX/boundsY in the same render — bounds re-parse against the current position.
  2. Don't rewrite bounds on every @move. The resolved range is absolute after first parse; re-applying a relative string mid-drag shifts the clamp. Sync bounds on @movestart/@moveend.
  3. "null" locks an axis to its current coordinate (it is not "no bounds").

Slots

  • default — content (wrapped in a full-size box so the host always has a hit area).
  • handle — optional drag handle. When present, only the handle starts a drag.
<v-movable>
  <template #handle><div class="titlebar">drag from here</div></template>
  <div>not grabbable</div>
</v-movable>

Examples

Move a parent (modal title)

<template>
  <div id="dialog" style="position:absolute;width:220px;border:1px solid #57c">
    <v-movable target-selector="#dialog">
      <template #handle><div class="titlebar">Title</div></template>
    </v-movable>
    <div class="body">Body is not a handle.</div>
  </div>
</template>

Horizontal only

<v-movable axis="x" boundsX="-50,250"><div>Horizontal</div></v-movable>
<!-- equivalent -->
<v-movable boundsX="-50,250" boundsY="null"><div>Horizontal</div></v-movable>

Grid + Shift

<v-movable :grid="50" shift-behavior><div>Snap 50px (hold Shift)</div></v-movable>

Constrained box

Clamped to a 200×200 parent. At (100,100), "-100,100" → absolute [0,200].

<div style="position:relative;width:200px;height:200px;border:1px solid green">
  <v-movable :left="100" :top="100" boundsX="-100,100" boundsY="-100,100">
    <div>box</div>
  </v-movable>
</div>

Migrating from 0.x (Vue 2)

| 0.x | 1.0 | | --- | --- | | <movable> (Vue 2, directive-based) | <v-movable> (Vue 3, wraps lit-movable) | | :bounds="{x:[min,max],y:[min,max]}" | boundsX="min,max" / boundsY="min,max" (strings) | | vertical="[min,max]" | axis="y" + boundsY="min,max" | | horizontal="[min,max]" | axis="x" + boundsX="min,max" | | posTop / posLeft props | top / left (with v-model:top / v-model:left) | | @start / @move / @complete | @movestart / @move / @moveend | | shiftKey | shift-behavior | | target (Vue ref name) | target-selector (CSS selector) |

The package name stays v-movable. The build is now Vite (bili is gone).

Local development

git clone https://github.com/thewebkid/v-movable.git
cd v-movable
npm i
npm run dev      # demo app
npm test         # web-test-runner (real browser drags)
npm run build    # vite lib build → dist/

License

MIT