Skip to content

MagicDraggable

MagicDraggable is a touch enabled, draggable component that can snap to configurable points inside its container.

View source View as Markdown

Drag me
DefaultDemo.vue
<template>
  <magic-draggable id="magic-draggable-demo" class="bg-surface-base rounded-md">
    <div
      class="bg-surface-higher flex aspect-square w-32 items-center justify-center rounded-md"
    >
      <m-badge>Drag me</m-badge>
    </div>
  </magic-draggable>
</template>

<style>
[data-id='magic-draggable-demo'] {
  --magic-draggable-position: relative;
  --magic-draggable-z-index: 50;
  --magic-draggable-height: 20rem;
}
</style>

Anatomy

<template>
  <magic-draggable id="your-draggable-id">
    <!-- your content -->
  </magic-draggable>
</template>

<script setup>
const { open } = useMagicDraggable('your-draggable-id')
</script>

Installation

CLI

Add @maas/vue-equipment to your dependencies.

pnpm add @maas/vue-equipment

Vue

If you are using Vue, import and add MagicDraggablePlugin to your app.

import { createApp } from 'vue'
import { MagicDraggablePlugin } from '@maas/vue-equipment/plugins/MagicDraggable'

const app = createApp({})

app.use(MagicDraggablePlugin)

Nuxt

The component is available as a Nuxt module. In your Nuxt config file add @maas/vue-equipment/nuxt to your modules and add MagicDraggable to the plugins in your configuration.

export default defineNuxtConfig({
  modules: ['@maas/vue-equipment/nuxt'],
  vueEquipment: {
    plugins: ['MagicDraggable'],
  },
})

Direct Import

If you prefer a more granular approach, the component can also be directly imported into any Vue component.

<script setup>
import { MagicDraggable } from '@maas/vue-equipment/plugins/MagicDraggable'
</script>

<template>
  <magic-draggable id="your-draggable-id">
    <!-- your content -->
  </magic-draggable>
</template>

Composable

In order to interact with the component from anywhere within your app, we provide a useMagicDraggable composable. Import it directly when needed.

import { useMagicDraggable } from '@maas/vue-equipment/plugins/MagicDraggable'

const { snapTo } = useMagicDraggable('your-draggable-id')

function handleClick() {
  snapTo('top-right')
}

Peer Dependencies

MagicDraggable has no optional peer dependencies. It only relies on the core peer dependencies, which npm, pnpm and bun install automatically.

API Reference

Props

The component comes with a simple set of props. Only the id is required.

PropTypeRequired
id
MaybeRef<string>true
options
MagicDraggableOptionsfalse

Options

To customize the component, override the necessary options. Any custom options will be merged with the default options.

OptionTypeDefault
tag
string
'div'
threshold.distance
number128
threshold.momentum
number1.5
threshold.idle
number250
threshold.lock
number8
animation.snap.duration
number500
animation.snap.easing
function
easeOutBack
initial.snapPoint
DraggableSnapPoint
'center'
snapPoints
DraggableSnapPoint[]
DraggableSnapPoint[]
scrollLock
boolean | objectobject
scrollLock.padding
booleantrue
preventEdgeNavigation
booleantrue
disabled
booleanfalse

CSS Variables

In order to provide its basic functionality the component comes with some CSS. To ensure that the component is customizable, relevant values are available as CSS variables.

VariableDefault
--magic-draggable-positionfixed
--magic-draggable-width100%
--magic-draggable-height100%
--magic-draggable-z-index999
--magic-draggable-inset0
--magic-draggable-cursorgrab
--magic-draggable-cursor-dragginggrabbing

Errors

SourceErrorCodeMessage
MagicDraggablemissing_snap_pointMagicDraggable must have at least one snap point set

Caveats

On iOS, a touch that starts near a vertical viewport edge arms the browser’s back and forward navigation swipe — in Safari as well as in installed web apps. A draggable resting near either edge would lose its drag to that gesture, so it cancels touchstart within 44px of the edge, which is the one signal WebKit honors. Canceling also swallows the tap’s synthesized click; the draggable dispatches a replacement when the touch ends within 12px of where it started, so click listeners keep firing. A touch on an SVG icon counts as well, and its replacement click goes to the HTML element that holds the icon. Set preventEdgeNavigation to false to opt out.

Examples

Resize

ResizeDemo.vue
<template>
  <magic-draggable
    id="magic-draggable-resize-demo"
    class="bg-surface-base rounded-md"
    :options="{
      threshold: {
        lock: 10,
      },
    }"
  >
    <div
      :class="[
        'bg-surface-high flex w-40 items-center justify-center rounded-md transition-all duration-150',
        {
          'aspect-square': square,
          'aspect-[2/3]': !square,
        },
      ]"
    >
      <m-button @click="toggle">Resize</m-button>
    </div>
  </magic-draggable>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
import { MButton } from 'mirror-compat'

const square = ref(true)

function toggle() {
  square.value = !square.value
}
</script>

<style>
[data-id='magic-draggable-resize-demo'] {
  --magic-draggable-position: relative;
  --magic-draggable-z-index: 50;
  --magic-draggable-height: 20rem;
}
</style>

Snap Points

Drag me
SnapPointsDemo.vue
<template>
  <magic-draggable
    id="magic-draggable-snap-points-demo"
    :options="{
      snapPoints: ['top', 'bottom', 'left', 'right'],
      initial: { snapPoint: 'top' },
    }"
    class="bg-surface-base rounded-md"
  >
    <div
      class="bg-surface-high flex aspect-square w-32 items-center justify-center rounded-md"
    >
      <m-badge>Drag me</m-badge>
    </div>
  </magic-draggable>
</template>

<style>
[data-id='magic-draggable-snap-points-demo'] {
  --magic-draggable-position: relative;
  --magic-draggable-z-index: 50;
  --magic-draggable-height: 20rem;
}
</style>