vue-lasso
v0.1.0
Published
Lasso selection for Vue 3 — select DOM elements with a freeform lasso or marquee rectangle, or cut freehand regions out of images. Headless core, zero dependencies, TypeScript, SSR-safe.
Downloads
39
Maintainers
Readme
vue-lasso
Freeform lasso & marquee selection for Vue 3 — plus a Photoshop-style image lasso that cuts pixels out of any image. Headless core, zero dependencies, fully typed, SSR-safe.
- 🪢
<LassoArea>— drag a freeform lasso (or marquee rectangle) around DOM elements, get them inv-model - ✂️
<LassoImage>— draw on an image, get the region as canvas / Blob / data URL / CSSclip-path - 🧠 Headless —
useLasso/usePointerLassocomposables when you want your own rendering - 📐
vue-lasso/core— the pure geometry & selection math, importable without Vue - ⌨️ File-manager modifiers: Shift add · Ctrl/⌘ toggle · Alt subtract · Esc cancel
- 📱 Touch: long-press to lasso, so normal page scrolling keeps working; pen supported
- 🚀 ~10 kB gzipped, no runtime dependencies, tree-shakable
Install
npm install vue-lasso// Global registration (optional — everything is also a named export)
import { createApp } from 'vue'
import VueLasso from 'vue-lasso'
createApp(App).use(VueLasso).mount('#app')
// registers <LassoArea>, <LassoImage> and the v-lasso-item directiveQuick start — select elements
<script setup lang="ts">
import { ref } from 'vue'
import { LassoArea, vLassoItem } from 'vue-lasso'
const photos = ref([{ id: 1, name: 'A' }, { id: 2, name: 'B' } /* … */])
const selected = ref([])
</script>
<template>
<LassoArea v-model="selected">
<div
v-for="photo in photos"
:key="photo.id"
v-lasso-item="photo"
:class="{ active: selected.includes(photo) }"
>
{{ photo.name }}
</div>
</LassoArea>
</template>Drag anywhere in the area to draw a lasso — every element marked with v-lasso-item (or a data-lasso-item attribute) that the shape touches lands in selected. Clicking an item selects just that item; clicking empty space clears.
Three ways to mark selectable items:
| How | Selected values |
| --- | --- |
| v-lasso-item="anything" | whatever you bound (objects, ids, …) |
| <div data-lasso-item="42"> | the attribute string ("42") |
| data-lasso-item (empty) | the DOM element itself |
Quick start — cut a region out of an image
<script setup lang="ts">
import { LassoImage, type ImageLassoSelection } from 'vue-lasso'
async function onSelect(selection: ImageLassoSelection) {
const url = selection.toDataURL() // PNG of the cut-out region
const blob = await selection.toBlob() // …or a Blob
document.body.style.clipPath = selection.clipPath // …or a CSS polygon()
}
</script>
<template>
<LassoImage src="/photo.jpg" @select="onSelect" />
</template>The drawn path stays visible (marching ants) until the next gesture, a tap, or clear(). Cutouts are computed lazily at the image's natural resolution and cropped to the path's bounding box, transparent outside the path.
CORS note: to export pixels from cross-origin images, serve them with CORS headers and set
crossorigin="anonymous".
<LassoArea> API
Props
| Prop | Type / default | Description |
| --- | --- | --- |
| v-model | unknown[] | Selected item values |
| mode | 'lasso' \| 'rect' = 'lasso' | Freeform path or marquee rectangle |
| hitMode | 'intersect' \| 'contain' \| 'center' = 'intersect' | Overlap / fully-inside / center-inside |
| commit | 'live' \| 'end' = 'live' | Update v-model while dragging, or only on release |
| disabled | boolean = false | |
| itemSelector | string = '[data-lasso-item]' | Which descendants are selectable |
| items | () => LassoItem[] | Custom item source (rects/points in content space) — for canvas, charts, virtual lists |
| modifierKeys | boolean = true | Shift/Ctrl/Alt behaviors |
| clearOnClick | boolean = true | Plain click on empty space clears |
| clickToSelect | boolean = true | Click selects a single item |
| dragThreshold | number = 4 | px of movement before a drag becomes a lasso |
| touch | 'hold' \| 'immediate' \| 'none' = 'hold' | Touch strategy (see Touch) |
| touchHoldMs | number = 300 | Long-press duration for 'hold' |
| autoScroll | boolean = true | Scroll container/window near edges while dragging |
| autoScrollEdge / autoScrollSpeed | 28 / 22 | Edge zone (px) and max speed (px/frame) |
| tag | string = 'div' | Root element tag |
Events
| Event | Payload |
| --- | --- |
| start | { event } |
| change | { selected, added, removed, event } — throttled to animation frames |
| end | { selected, added, removed, canceled, event } |
| item-click | { value, selected, event } |
Slot props & exposed methods
Default slot receives { isSelecting, selected }. Template ref exposes selectAll(), clear(), cancel(), refresh() (re-measure mid-drag), isSelecting.
While dragging, items currently inside the shape get a data-lasso-hit attribute — style the live preview with plain CSS:
.card[data-lasso-hit] { outline: 2px dashed #528fff; }<LassoImage> API
Props: src (required), alt, crossorigin, mode ('lasso' | 'rect' — rect gives you a crop box), feather (px, soft edge), keepSelection (true), disabled, touch, touchHoldMs, dragThreshold.
Events: select(ImageLassoSelection), change({ path, event }), start, cancel, load, error.
interface ImageLassoSelection {
path: Point[] // displayed-image coordinates
imagePath: Point[] // natural-pixel coordinates
bounds: Rect // natural-pixel bounding box of the cutout
clipPath: string // CSS polygon(evenodd, …) in percentages
toCanvas(opts?: { feather?: number; invert?: boolean }): HTMLCanvasElement
toMaskCanvas(opts?): HTMLCanvasElement // white-on-transparent alpha mask
toDataURL(opts?: { type?: string; quality?: number; feather?; invert? }): string
toBlob(opts?): Promise<Blob>
}invert: true keeps everything except the drawn region. The standalone helpers are exported too: createCutout(image, path, opts), createMaskCanvas, pathToClipPath.
Headless usage
useLasso gives you the full selection engine without the component:
import { ref } from 'vue'
import { useLasso } from 'vue-lasso'
const container = ref<HTMLElement>()
const { selected, isSelecting, pathD, selectAll, clear } = useLasso(container, {
hitMode: 'center',
onEnd: ({ selected }) => console.log(selected),
})For chart/canvas scenarios, supply your own items (content-space rects or points):
useLasso(container, {
items: () => dataPoints.map((d) => ({ value: d, point: { x: d.px, y: d.py } })),
})usePointerLasso is one level lower — it only turns pointer gestures into a content-space path (points, selectionRect, pathD, callbacks), and is what both components are built on. Render the shape however you like.
vue-lasso/core exports the pure math with no Vue import: pointInPolygon, polygonIntersectsRect, polygonContainsRect, segmentsIntersect, combineSelection, behaviorFromModifiers, …
Styling
A tiny stylesheet is injected automatically on first mount (no CSS import needed). Customize via CSS variables on the container:
| Variable | Default | |
| --- | --- | --- |
| --lasso-stroke | #528fff | path stroke color |
| --lasso-fill | rgba(82,143,255,.12) | path fill |
| --lasso-stroke-width | 1.5 | |
| --lasso-dash | 5 4 | dash pattern (marching ants) |
| --lasso-ants-speed | .5s | animation speed (disabled with prefers-reduced-motion) |
| --lasso-z-index | 999 | overlay z-index |
| --lasso-cursor | crosshair | cursor while selecting |
Classes: lasso-area, lasso-active (during a gesture), lasso-overlay, lasso-path, lasso-image. Elements matching [data-lasso-ignore], inputs, buttons and links never start a lasso.
Touch behavior
Lassoing and scroll-panning fight over the same gesture. The default touch: 'hold' resolves it the way photo apps do: long-press (300 ms), then drag to lasso, while a normal swipe scrolls the page untouched. Use 'immediate' in UIs that don't scroll, or 'none' to keep touch scroll-only. Mouse and pen are always immediate.
SSR / Nuxt
Everything is SSR-safe: no window/document access at module scope, style injection happens on mount, and v-lasso-item ships getSSRProps so server markup matches the client. Just import and use.
Performance notes
- Item rects are measured once per gesture and hit-tested against the shape on animation frames only.
- Lasso points are thinned (
minPointDistance) and capped (maxPoints), with polygon-bounds quick rejection before exact math. - A few thousand items are fine; for huge virtual lists supply
itemsyourself. - Call the exposed
refresh()if your layout reflows mid-drag.
FAQ
Esc closed my lasso and my modal. No — while a lasso is active, Esc is intercepted (capture phase) and only cancels the lasso.
A click fires after I finish dragging. One synthetic click after each drag is swallowed automatically so your @click handlers don't double-fire.
Range-select with Shift+click? Shift+click currently adds the clicked item. True range semantics depend on item order — track item-click if you need it.
Roadmap
Polygon (click-to-place-points) mode · magnetic edge snapping for images · spatial index for 10k+ items. PRs welcome.
License
MIT © 2026 Hareram Ray
