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

lit-movable

v1.0.2

Published

Movable element web component - simple and robust. Customize how any element moves.

Readme

<movable-el> npm version tests License: MIT

Declarative drag/move for Lit and plain HTML. Wrap content in <movable-el>, optionally constrain axes, snap to a grid, or move a different target than the drag handle.

When to use it: Lit / web-component apps that need lightweight pointer dragging with rich move state — without a full drag-and-drop framework.

Live Demo

Peer dependency: lit ^3 (not bundled).
Tags: primary <movable-el>; <lit-movable> remains registered as a compatible alias.
TypeScript: ships with index.d.ts (Movable, MoveState, tag map for both elements).

Installation

npm i lit-movable
import { Movable, type MoveState } from 'lit-movable';

const el = document.querySelector('movable-el');
el?.addEventListener('move', (e: CustomEvent<MoveState>) => {
  console.log(e.detail.coords);
});

Basic usage

<script type="module">
  import { Movable } from 'lit-movable';
</script>

<movable-el>
  <div style="background:lightsteelblue">I am movable</div>
</movable-el>

Attributes

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

Bounds mental model (read this)

boundsX / boundsY are not absolute style.left / style.top ranges.

They are deltas from the element’s current position at the moment the attribute/property is applied:

absoluteMin = currentLeft + min
absoluteMax = currentLeft + max

So if the knob is already at left: 85 and you want it clamped to the box [0, 160]:

<!-- WRONG — looks absolute, parses as [85, 245] -->
<movable-el posLeft="85" boundsX="0,160"></movable-el>

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

General recipe for “stay inside [0, size]” while the control is at (left, top):

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

Lit / reactive gotchas

  1. Set posLeft / posTop before boundsX / boundsY in the same render. Bounds reparse against current style.left / top. If bounds land first, the offset is stale and the clamp drifts.
  2. Do not rewrite bounds on every move event. The resolved [min, max] is already absolute after the first parse. Re-applying a new relative string mid-drag (while style.left has moved but your bound pos* lags) widens or shifts the clamp — knobs escape the box, saturation goes negative, etc. Sync bounds on movestart / moveend (or whenever you intentionally reposition outside a gesture), and only update pos* during move.
  3. "null" locks an axis to the current coordinate (no movement on that axis), not “no bounds”.
<!-- Horizontal slider: free X in a band, Y locked -->
<movable-el posLeft="40" axis="x" boundsX="-40,200">
  <a class="thumb"></a>
</movable-el>

Slots

  • default — content
  • handle — optional drag handle. When present, only that slot starts a drag
<movable-el>
  <div slot="handle">Drag me</div>
  <div>I move with the handle, but I'm not grabbable</div>
</movable-el>

Events

Custom events bubble and are composed. event.detail is a plain move-state object (not a spread PointerEvent):

  • coords, startCoord, moveDist, totalDist, mouseCoord, clickOffset
  • posTop, posLeft, pctX / pctY (when bounds are finite), isMoving

Events: movestart, move, moveend.

Callback properties onmovestart, onmove, onmoveend receive the same state object.

const el = document.querySelector('movable-el');

el.addEventListener('move', ({ detail }) => {
  console.log(detail.coords, detail.totalDist);
});

el.onmoveend = (state) => console.log(state.posLeft, state.posTop);

Examples

Move a parent (modal title)

<div id="dialog" style="position:absolute;width:200px;border:1px solid blue">
  <movable-el targetSelector="#dialog">
    <div slot="handle" style="background:lightsteelblue">Title</div>
  </movable-el>
  Body is not a handle.
</div>

Horizontal only

<movable-el axis="x" boundsX="-50,250">
  <div>Horizontal</div>
</movable-el>

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

Grid + shift

<movable-el grid="50" shiftBehavior>
  <div>Snap 50px (hold Shift)</div>
</movable-el>

Constrained box

Clamped to a 200×200 parent. Note the relative bounds: at (100,100), "-100,100" → absolute [0,200].

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

Reactive knob (color picker pattern)

Keep the sample point on-canvas; allow the thumb to half-overhang. Freeze bounds during the gesture:

// size = canvas CSS px; left/top = sample point
const boundsX = `${-left}, ${size - left}`;
const boundsY = `${-top}, ${size - top}`;

html`
  <movable-el
    .posTop=${top}
    .posLeft=${left}
    .boundsX=${dragging ? frozenBoundsX : boundsX}
    .boundsY=${dragging ? frozenBoundsY : boundsY}
    @movestart=${() => { dragging = true; freezeBounds(); }}
    @move=${onMove}
    @moveend=${() => { dragging = false; }}>
    <div class="circle"></div>
  </movable-el>
`;

Migrating from 0.x

| 0.x | 1.0 | |-----|-----| | <lit-movable> | <movable-el> (or keep <lit-movable> — still registered as an alias) | | import { LitMovable } | import { Movable } (LitMovable still exported as the alias class) | | horizontal="min,max" | axis="x" + boundsX="min,max" | | vertical="min,max" | axis="y" + boundsY="min,max" | | event detail mixed with PointerEvent | plain move-state only |

npm package name remains lit-movable.

Local development

git clone https://github.com/thewebkid/lit-movable.git
cd lit-movable
npm i
npm run dev
npm test
npm run build