Skip to content
RTL Support: Full
Accessibility: Full
Translations: Not Needed

Draggable

Atomic draggable element that provides visual structure and styling for draggable content. Use this component within a drag and drop library to provide visual feedback and structure for draggable content.

See our recommended drag and drop library here.

Default

Render a bordered element with a drag handle by placing your content in the #content slot.

Visual structure for draggable content
vue
<template>
  <HLDraggableElement id="draggable-default">
    <template #content>
      <div>Visual structure for draggable content</div>
    </template>
  </HLDraggableElement>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Handle Visibility

Use showHandler to control whether the drag handle is rendered. When it is true (the default), only the handle is draggable (cursor: grab) and the rest of the element stays static — pair it with your library's handle option. When it is false, the handle is hidden and the whole element becomes the drag target.

Handle shown — drag by the handle only
No handle — the whole element is draggable
vue
<template>
  <!-- Handle shown (default): drag by the handle only -->
  <HLDraggableElement id="draggable-handle-on">
    <template #content>
      <div>Handle shown — drag by the handle only</div>
    </template>
  </HLDraggableElement>

  <!-- Handle hidden: the whole element is draggable -->
  <HLDraggableElement id="draggable-handle-off" :show-handler="false">
    <template #content>
      <div>No handle — the whole element is draggable</div>
    </template>
  </HLDraggableElement>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Borderless

By default the element has a border. Set :bordered="false" to remove it — useful when the element sits inside another bordered container or when you want a flatter look.

Bordered (default)
Borderless
vue
<template>
  <HLDraggableElement id="draggable-bordered">
    <template #content>
      <div>Bordered (default)</div>
    </template>
  </HLDraggableElement>

  <HLDraggableElement id="draggable-borderless" :bordered="false">
    <template #content>
      <div>Borderless</div>
    </template>
  </HLDraggableElement>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Handler Positions

The component supports different handler positions to match your drag and drop implementation needs.

Left handler
Right handler
Top handler
Bottom handler
vue
<template>
  <div class="space-y-4">
    <HLDraggableElement id="draggable-left" handler-position="left">
      <template #content>
        <div>Left handler</div>
      </template>
    </HLDraggableElement>
    <HLDraggableElement id="draggable-right" handler-position="right">
      <template #content>
        <div>Right handler</div>
      </template>
    </HLDraggableElement>
    <HLDraggableElement id="draggable-top" handler-position="top">
      <template #content>
        <div>Top handler</div>
      </template>
    </HLDraggableElement>
    <HLDraggableElement id="draggable-bottom" handler-position="bottom">
      <template #content>
        <div>Bottom handler</div>
      </template>
    </HLDraggableElement>
  </div>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Visual States

The component provides visual feedback states that can be controlled by your drag and drop implementation.

Disabled state
Visual feedback for dragging state
Visual feedback for droppable state
vue
<template>
  <div class="space-y-4">
    <HLDraggableElement id="draggable-disabled" disabled>
      <template #content>
        <div>Disabled state</div>
      </template>
    </HLDraggableElement>
    <HLDraggableElement id="draggable-dragging" :is-dragging="true">
      <template #content>
        <div>Visual feedback for dragging state</div>
      </template>
    </HLDraggableElement>
    <HLDraggableElement id="draggable-droppable" :is-droppable="true">
      <template #content>
        <div>Visual feedback for droppable state</div>
      </template>
    </HLDraggableElement>
  </div>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Full Example with a Drag-and-Drop Library

HLDraggableElement is presentational only — it renders the handle, border, and drag/drop visual states, but does not implement the dragging itself. Pair it with a drag-and-drop library to build a real interaction. This example uses vue-draggable-next, the recommended library.

The board below has two lists that share a group, so cards can be reordered within a list or moved between lists — a typical "To do / Done" pattern. Two of the component's visual states are wired up:

  • isDragging — the card currently being dragged is dimmed/lifted.
  • isDroppable — while a drag is in progress, both lists highlight to show they are valid drop targets.

Drag a card by its handle:

vue
<template>
  <div class="board">
    <!-- To do -->
    <div class="column">
      <p class="column-title">To do</p>
      <div class="drop-zone">
        <HLDraggableElement :is-droppable="isDragActive" :bordered="false" :show-handler="false">
          <template #content>
            <VueDraggableNext v-model="todo" group="tasks" handle=".hr-draggable__handler" class="stack" @start="onDragStart" @end="onDragEnd">
              <HLDraggableElement
                v-for="task in todo"
                :key="task.id"
                :id="`todo-${task.id}`"
                :is-dragging="draggingId === `todo-${task.id}`"
              >
                <template #content>
                  <div class="px-2">{{ task.label }}</div>
                </template>
              </HLDraggableElement>
            </VueDraggableNext>
          </template>
        </HLDraggableElement>
      </div>
    </div>

    <!-- Done -->
    <div class="column">
      <p class="column-title">Done</p>
      <div class="drop-zone">
        <HLDraggableElement :is-droppable="isDragActive" :bordered="false" :show-handler="false">
          <template #content>
            <VueDraggableNext v-model="done" group="tasks" handle=".hr-draggable__handler" class="stack" @start="onDragStart" @end="onDragEnd">
              <HLDraggableElement
                v-for="task in done"
                :key="task.id"
                :id="`done-${task.id}`"
                :is-dragging="draggingId === `done-${task.id}`"
              >
                <template #content>
                  <div class="px-2">{{ task.label }}</div>
                </template>
              </HLDraggableElement>
            </VueDraggableNext>
          </template>
        </HLDraggableElement>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
  import { ref } from 'vue'
  import { VueDraggableNext } from 'vue-draggable-next'
  import { HLDraggableElement } from '@platform-ui/highrise'

  interface Task {
    id: number
    label: string
  }

  const todo = ref<Task[]>([
    { id: 1, label: 'Design review' },
    { id: 2, label: 'Write release notes' },
    { id: 3, label: 'Update dependencies' },
  ])
  const done = ref<Task[]>([{ id: 4, label: 'Fix login bug' }])

  // The card being dragged (drives isDragging)
  const draggingId = ref<string | null>(null)
  // Whether any drag is in progress (drives isDroppable on both lists)
  const isDragActive = ref(false)

  const onDragStart = (event: any) => {
    // event.item is the dragged card's root .hr-draggable-element;
    // its `id` is set from the id prop (e.g. "todo-1")
    draggingId.value = event.item.id
    isDragActive.value = true
  }
  const onDragEnd = () => {
    draggingId.value = null
    isDragActive.value = false
  }
</script>

<style scoped>
  .board {
    display: flex;
    gap: 16px;
    align-items: flex-start;
  }
  .column {
    flex: 1;
  }
  .column-title {
    font-weight: 600;
    margin: 0 0 8px;
  }
  .drop-zone {
    min-height: 60px;
    padding: 8px;
    background: var(--gray-50);
    border-radius: 8px;
  }
  /* Space the cards inside each list */
  .stack {
    display: flex;
    flex-direction: column;
    gap: 8px;
    min-height: 44px;
    width: 100%;
  }
</style>

INFO

  • group="tasks" — giving both lists the same group name is what lets cards move between them. Reordering within one list works with or without a group.
  • handle=".hr-draggable__handler" limits dragging to the built-in handle, keeping the rest of the card clickable. HLDraggableElement renders its handle with the hr-draggable__handler class, so no extra markup is needed.
  • The outer HLDraggableElement here is used purely as a styled drop zone (:show-handler="false", :bordered="false") whose isDroppable state highlights it during a drag — the inner elements are the actual draggable cards. It is wrapped in a plain <div class="drop-zone"> because layout class/style set directly on HLDraggableElement are not forwarded to its root element (see the note below).
  • onDragStart reads event.item.idvue-draggable-next reports the dragged card's root element, whose id comes from the id prop. Use id (a real prop) rather than a data-* attribute, since arbitrary attributes are not passed through.

WARNING

HLDraggableElement only applies the id prop to its root element. Any other attribute you place directly on the tag — class, style, data-*, aria-*, role, tabindex — is dropped and never reaches the DOM. To add layout classes, styling, or drag-library data hooks, wrap the component in your own element (as the drop zones do above), or target it via a unique id.

Custom Handler Icon

Fill the #icon slot to replace the default drag handle with your own icon.

↕️
Custom drag handler icon
vue
<template>
  <HLDraggableElement id="draggable-custom-icon">
    <template #icon>
      <div class="p-2">↕️</div>
    </template>
    <template #content>
      <div>Custom drag handler icon</div>
    </template>
  </HLDraggableElement>
</template>

<script setup lang="ts">
  import { HLDraggableElement } from '@platform-ui/highrise'
</script>

Accessibility

Because HLDraggableElement does not forward aria-* attributes to its root (see the warning above), put ARIA on your own markup — either on a wrapping element around the component or on the content you render inside the #content / #icon slots.

  • Label each draggable item with aria-label / aria-labelledby on a wrapping element or inside the slot content so users know what will move when they grab it.
  • Toggle aria-grabbed (or aria-pressed if you use a drag-mode switch) at the start/end of the drag on that same element.
  • Reference drop targets via aria-controls / aria-describedby and include helper copy that explains how to complete the drop.

Imports

ts
import { HLDraggableElement } from '@platform-ui/highrise'

Props

NameTypeDefaultDescription
idstring | undefinedundefinedUnique identifier for the element
borderedbooleantrueWhether to show border around the element
showHandlerbooleantrueWhether to show the drag handler
handlerPosition'left' | 'right' | 'top' | 'bottom''left'Position of the drag handler
disabledbooleanfalseWhether the element is disabled
isDraggingbooleanfalseWhether the element is being dragged
isDroppablebooleanfalseWhether the element can be dropped

Slots

NameParametersDescription
content()The element's main content. This is where you render your row/card — an element with no content shows only the handle
icon()Custom icon for the drag handle. Falls back to the built-in drag icon when omitted, and is only rendered while showHandler is true