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.
<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.
<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.
<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.
<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.
<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:
<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.HLDraggableElementrenders its handle with thehr-draggable__handlerclass, so no extra markup is needed.- The outer
HLDraggableElementhere is used purely as a styled drop zone (:show-handler="false",:bordered="false") whoseisDroppablestate highlights it during a drag — the inner elements are the actual draggable cards. It is wrapped in a plain<div class="drop-zone">because layoutclass/styleset directly onHLDraggableElementare not forwarded to its root element (see the note below). onDragStartreadsevent.item.id—vue-draggable-nextreports the dragged card's root element, whoseidcomes from theidprop. Useid(a real prop) rather than adata-*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.
<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-labelledbyon a wrapping element or inside the slot content so users know what will move when they grab it. - Toggle
aria-grabbed(oraria-pressedif you use a drag-mode switch) at the start/end of the drag on that same element. - Reference drop targets via
aria-controls/aria-describedbyand include helper copy that explains how to complete the drop.
Imports
import { HLDraggableElement } from '@platform-ui/highrise'Props
| Name | Type | Default | Description |
|---|---|---|---|
| id | string | undefined | undefined | Unique identifier for the element |
| bordered | boolean | true | Whether to show border around the element |
| showHandler | boolean | true | Whether to show the drag handler |
| handlerPosition | 'left' | 'right' | 'top' | 'bottom' | 'left' | Position of the drag handler |
| disabled | boolean | false | Whether the element is disabled |
| isDragging | boolean | false | Whether the element is being dragged |
| isDroppable | boolean | false | Whether the element can be dropped |
Slots
| Name | Parameters | Description |
|---|---|---|
| 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 |