const DnDProvider: Component<DnDProviderProps>;안에서 렌더링되는 모든 것의 드래그를 관리합니다: 입력, 오버레이, 선택, 자동 스크롤, 스크린 리더 안내 문구(DnDProviderProps 참고).
<script setup lang="ts">
import { DnDProvider } from '@vue-dnd-kit/core';
</script>
<template>
<DnDProvider :animation="{ duration: 200 }" @drag-end="save">
<RouterView />
<template #overlay="{ items, over, accepted }">
<!-- optional: a custom overlay for every drag -->
</template>
</DnDProvider>
</template>
모든 컴포저블은 프로바이더 안에서 렌더링되는 컴포넌트에서 실행되어야 합니다. 앱 전체에
프로바이더 하나를 두는 것이 목표입니다. 페이지 일부의 규칙은
makeDragArea로 따로 지정합니다. 프로바이더 가이드를
참고하세요.
대부분의 props는 프로바이더가 필요할 때 읽습니다. 하나를 바꾸면 다음 드래그부터 적용됩니다.
데모 불러오는 중…
<script setup lang="ts">
import { computed, ref } from 'vue';
import { DnDProvider, type ActivationOptions } from '@vue-dnd-kit/core';
import PropRow from './PropRow.vue';
import DensityList from './DensityList.vue';
const ACTIVATIONS: Record<string, ActivationOptions | undefined> = {
default: undefined,
'{ mouse: { delay: 300 } }': { mouse: { delay: 300, tolerance: 5 } },
};
const overlayFit = ref<'slot' | 'source'>('slot');
const activation = ref('default');
const keyboard = ref<'true' | 'false'>('true');
const copyModifier = ref<'Alt' | 'Shift' | 'null'>('Alt');
// The provider reads its props when it needs them: a change applies to the next drag.
const props = computed(() => ({
overlayFit: overlayFit.value,
activation: ACTIVATIONS[activation.value],
// `false` turns keyboard drags off; an object sets their keys and steps.
keyboard: keyboard.value === 'true' ? undefined : (false as const),
copyModifier: copyModifier.value === 'null' ? null : copyModifier.value,
}));
const compact = ref([
{ id: 'copy', title: 'Write copy' },
{ id: 'login', title: 'Fix login' },
]);
const roomy = ref([
{ id: 'sprint', title: 'Plan the sprint' },
{ id: 'review', title: 'Review the PR' },
]);
</script>
<template>
<DnDProvider v-bind="props">
<div class="demo">
<div class="props" role="group" aria-label="DnDProvider props">
<PropRow v-model="overlayFit" name="overlay-fit" :options="['slot', 'source']" />
<PropRow v-model="activation" name=":activation" :options="Object.keys(ACTIVATIONS)" />
<PropRow v-model="keyboard" name=":keyboard" :options="['true', 'false']" />
<PropRow v-model="copyModifier" name="copy-modifier" :options="['Alt', 'Shift', 'null']" />
</div>
<div class="lists">
<DensityList v-model="compact" title="Compact" compact />
<DensityList v-model="roomy" title="Roomy" />
</div>
</div>
</DnDProvider>
</template>
<style scoped>
/* Room for every row in one list: the lists grow downward and nothing moves under the pointer. */
.demo {
display: flex;
flex-direction: column;
align-items: center;
gap: 18px;
min-height: 430px;
}
.props {
display: grid;
grid-template-columns: auto auto;
align-items: center;
gap: 6px 14px;
max-width: 100%;
padding: 12px 14px;
border: 1px solid color-mix(in srgb, currentColor 12%, transparent);
border-radius: 12px;
background: color-mix(in srgb, currentColor 3%, Canvas);
}
.lists {
display: flex;
flex-wrap: wrap;
align-items: flex-start;
justify-content: center;
gap: 14px;
}
@media (max-width: 480px) {
.props {
grid-template-columns: minmax(0, 1fr);
}
}
</style>오버레이가 렌더링되는 곳
overlayTo는 오버레이를 body 밖으로 옮깁니다. 예를 들어 페이지의 다른 모든 것보다 위에 있는 모달
<dialog> 안으로 옮깁니다. 스위치를 끄면 드래그하는 행이 다이얼로그 아래로 사라집니다. 언제 필요한지는
오버레이 가이드에서 설명합니다.
데모 불러오는 중…
<script setup lang="ts">
import { computed, ref, useTemplateRef } from 'vue';
import { DnDProvider } from '@vue-dnd-kit/core';
import ColumnList from './ColumnList.vue';
const columns = ref(['Name', 'Status', 'Owner', 'Due date', 'Priority']);
const dialog = useTemplateRef<HTMLDialogElement>('dialog');
const open = ref(false);
const intoDialog = ref(true);
// A modal <dialog> sits on the top layer, above everything in <body>, the overlay
// included. While it is open, the overlay renders inside it.
const overlayTo = computed(() => (open.value && intoDialog.value ? '#columns-dialog' : 'body'));
const show = () => {
dialog.value?.showModal();
open.value = true;
};
</script>
<template>
<DnDProvider :overlay-to="overlayTo">
<div class="sheet">
<table>
<thead>
<tr>
<th v-for="column in columns" :key="column">{{ column }}</th>
</tr>
</thead>
<tbody>
<tr v-for="row in 2" :key="row">
<td v-for="column in columns" :key="column"><span class="skeleton" /></td>
</tr>
</tbody>
</table>
<button type="button" class="primary" @click="show">Edit columns</button>
</div>
<dialog id="columns-dialog" ref="dialog" @close="open = false">
<h3>Columns</h3>
<p>Drag to change their order.</p>
<ColumnList v-model:columns="columns" />
<footer>
<label class="switch">
<input v-model="intoDialog" type="checkbox" />
Overlay inside the dialog
</label>
<button type="button" class="primary" @click="dialog?.close()">Done</button>
</footer>
</dialog>
</DnDProvider>
</template>
<style scoped>
.sheet {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
width: 100%;
}
table {
width: min(100%, 520px);
border-collapse: collapse;
font-size: 12px;
}
th {
padding: 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 15%, transparent);
font-weight: 600;
text-align: left;
white-space: nowrap;
}
td {
padding: 10px 8px;
border-bottom: 1px solid color-mix(in srgb, currentColor 8%, transparent);
}
.skeleton {
display: block;
height: 8px;
border-radius: 4px;
background: color-mix(in srgb, currentColor 10%, transparent);
}
button {
padding: 7px 14px;
border: 0;
border-radius: 8px;
font: 600 12px/1.2 system-ui, sans-serif;
cursor: pointer;
}
.primary {
background: #42b883;
color: #04130c;
}
button:focus-visible {
outline: 2px solid #42b883;
outline-offset: 2px;
}
/* No transform on the dialog: it would move the overlay's fixed position. */
dialog {
width: min(340px, calc(100vw - 32px));
box-sizing: border-box;
margin: auto;
padding: 18px;
border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
border-radius: 16px;
background: Canvas;
color: CanvasText;
box-shadow: 0 30px 60px -20px rgb(0 0 0 / 0.6);
}
dialog::backdrop {
background: rgb(0 0 0 / 0.45);
}
h3 {
margin: 0;
font-size: 15px;
}
p {
margin: 4px 0 14px;
font-size: 12px;
opacity: 0.6;
}
footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-top: 16px;
}
.switch {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
}
.switch input {
accent-color: #42b883;
}
</style>Props
DnDProvider의 props. 대부분은 프로바이더가 필요할 때 읽으므로, 하나를 바꾸면 다음 드래그부터 그 값을 씁니다.
CollisionDetection드래그 아래의 대상을 고르는 방식. 기본값 pointerFirst. 다른 내장 방식(closestCorners)이나 직접 만든 것을 쓸 수 있습니다.
Modifier[]모든 드래그에 쓰는 모디파이어로, 가장 먼저 적용됩니다. 드래그 영역, draggable, 리스트의 모디파이어는 그 뒤에 옵니다.
LayoutAnimation정렬 가능한 리스트의 기본 FLIP 애니메이션.
DropAnimation드롭할 때 오버레이가 안착하고 취소할 때 원래 자리로 돌아가는 방식으로, 더 가까운 곳에서 정하지 않았을 때 쓰입니다. 드롭 대상의 overDropAnimation이 먼저이고, 그다음은 드래그 중인 항목 자체의 것(또는 그 리스트의 것), 마지막이 이것입니다.
AutoAnimation정렬 가능한 리스트와 트리의 기본 animateChanges: 드래그가 아닌 항목 변경. 기본으로 꺼져 있습니다.
boolean | AutoScrollOptions드래그가 가장자리에 가까워지면 컨테이너와 페이지를 스크롤합니다. 기본값 true. 옵션으로 조정하고, false로 끕니다. 리스트, 트리, 드래그 영역에서 재정의할 수 있습니다.
ActivationOptions누르기가 드래그가 되기 전에 충족해야 하는 조건으로, 포인터 종류별로 정합니다(기본값은 ActivationOptions 참고). 드래그 영역, 리스트, 트리, 항목, draggable에서 재정의할 수 있습니다.
KeyboardOptions | false키보드 드래그: 그 키와 이동 단위. false로 끕니다. Escape는 여전히 모든 드래그를 취소합니다.
Sensor[]입력을 드래그로 바꾸는 것. 기본값 [pointerSensor, keyboardSensor]. 하나를 빼거나, 감싸거나, 교체하거나, 직접 만든 센서(defineSensor)를 추가하세요. 키보드 센서는 Escape로 모든 드래그를 취소하는 역할도 합니다. keyboard: false는 키보드 드래그를 끄지만 이 역할은 유지합니다. 목록은 언제든 바뀔 수 있으며, 목록에서 빠진 센서는 진행 중인 드래그를 취소하고 멈춥니다.
boolean | HapticsOptions터치 드래그의 진동. 기본값: 길게 누른 뒤 드래그가 시작될 때 짧게 한 번. 객체를 넘기면 다른 진동(enter, drop, refuse)을 추가하고, false는 모두 끕니다.
string | null이동을 복사로 바꾸는 키(KeyboardEvent.key). 기본값 'Alt'. null이면 비활성화합니다.
string | HTMLElement오버레이를 렌더링할 곳: 선택자 또는 요소. 기본값 'body'. 모달 <dialog>는 페이지의 나머지 부분 위에 놓입니다. 그 안에서 드래그하려면 오버레이도 그 안에 있어야 합니다.
'slot' | 'source'드래그 중 오버레이의 크기. 'slot'(기본값): 항목이 안착할 자리를 따르므로, 다른 리스트 위에서는 그 리스트 항목의 크기가 되고 드롭할 때 크기를 바꿀 필요가 없습니다. 포인터를 중심으로 크기가 바뀌며, 포인터는 박스 안에서 같은 비율의 위치에 머무릅니다. 'source': 드래그 중인 요소의 크기를 유지합니다. 기본 오버레이 콘텐츠는 새 크기에 맞춰 다시 배치되며, 확대·축소되지 않습니다.
number | 'all'멀티 드래그: 집을 때 클론을 받는 드래그 항목의 수로, 집은 항목과 드롭 순서상 그 옆에 있는 다른 항목들이 대상입니다. 기본값은 5이고, 'all'은 렌더링된 항목을 모두 클론합니다. 기본 오버레이는 클론을 보여 주고 나머지는 개수로 셉니다(+N). 직접 만든 레이아웃에서는 클론이 없는 항목에 대해 DragPreview가 자신의 슬롯을 렌더링합니다. 모든 클론이 날아갑니다. 취소하면 자기 항목으로 돌아가고, 드롭하면 자기 자리로 가며, 드롭이 들어가는 영역으로는 하나씩 차례로 들어갑니다. 클론은 한꺼번에 만들어지고 스냅샷 한 번에 레이아웃도 전체에 한 번이지만, 클론마다 스타일을 읽는 비용이 듭니다. 느린 휴대폰에서는 긴 항목에 주의하세요.
boolean | TransitionProps드래그 중에 오버레이가 한 컴포넌트에서 다른 컴포넌트로 바뀌는 방식(드롭 대상의 overOverlay가 넘겨받거나, overlay ref가 바뀔 때). true(기본값)는 150 ms 동안 크로스페이드하고, false는 바로 교체하며, 객체는 내용의 루트를 감싸는 Vue <Transition>의 props입니다(name, mode, GSAP이나 motion용 JS 훅). mode: 'out-in'이 없으면 둘이 동시에 보입니다. 떠나는 쪽에 position: absolute를 주어(.<name>-leave-active) 박스 안에서 겹치게 하세요.
string | Record<string, boolean> | Array<string | Record<string, boolean>>오버레이 콘텐츠([data-dnd-overlay-content])의 클래스. 오버레이 박스를 채우는 요소로, 라이브러리가 박스를 옮기고 크기를 바꾸는 동안 전체를 애니메이션할 대상입니다. 이 요소의 트랜지션은 드래그가 집힐 때 @starting-style에서 실행되고, 안착할 때 거꾸로 실행됩니다([data-dnd-overlay='dropping']). 드롭은 이 트랜지션을 기다립니다. 일반 CSS는 <style scoped> 밖에서(또는 :global(...)로), Tailwind는 starting:과 in-data-[dnd-overlay=dragging]:으로 이 요소를 지정합니다.
Partial<Announcements> | false일부 이벤트에 쓸 직접 만든 문구(Announcements 참고). false면 라이브 리전이 아무것도 읽지 않습니다.
string키보드로 드래그하는 방법. 스크린 리더가 모든 draggable에 대해 읽어 줍니다(그 aria-describedby). 기본값: Space, Enter, 화살표 키, Escape에 대한 영어 안내.
boolean기본 모양을 주입합니다: 명시도가 0인 레이어드 CSS로, 플레이스홀더와 멀티 드래그가 제자리에 남긴 항목을 흐리게 합니다(opacity: 0.4). 기본값 true. false이면 모양은 직접 정합니다. 드래그에 필요한 것은 어느 쪽이든 주입됩니다: 드래그 중 텍스트 선택과 스크롤 앵커링 방지(가상 리스트가 이에 의존합니다), 드래그 커서(--dnd-cursor), iOS에서 길게 누를 때 링크 콜아웃 방지.
이벤트
| 이벤트 | 페이로드 | 시점 |
|---|---|---|
drag-start | DnDEvent | 드래그가 시작되었습니다(활성화 이후). |
drag-move | DnDEvent | 드래그 중에 포인터가 움직였습니다(최대 프레임당 한 번). |
drag-over | DnDEvent | 드래그 아래의 대상이 바뀌었습니다. |
drag-end | DnDEndEvent | 드롭이 완료되고 기록되었습니다. |
drag-cancel | DnDEvent | 드래그가 취소되었거나 드롭이 거부되었습니다. |
drag-resume | DnDEvent | onBeforeDrop이 'continue'로 답했습니다. 드롭은 없었고 드래그가 계속됩니다. |
drag-settle | DnDEndEvent 또는 DnDEvent | 드래그의 오버레이가 안착했습니다. drag-end나 drag-cancel이 받은 것과 같습니다. |
같은 이벤트와 함께 onBeforeDrop도 useDnDMonitor로
프로바이더 아래 어디서든 받을 수 있습니다. 페이로드는 이벤트 페이지에 있습니다.
#overlay 슬롯
자체 오버레이 컴포넌트가 없는 모든 드래그에서 포인터를 따라다니는 것을 렌더링합니다 (오버레이 가이드 참고). 아무것도 렌더링하지 않으면 클론을 사용합니다.
오버레이가 보여 주는 것: 프로바이더의 #overlay 슬롯과 오버레이 컴포넌트(draggable, 정렬 가능한 항목, 리스트, 트리의 overlay 옵션)의 props.
Active현재 드래그.
DragItem[]드래그 중인 모든 항목. 주 항목이 맨 앞입니다. 멀티 드래그 전체에 오버레이는 하나입니다.
Over | null드래그 아래의 드롭 대상. 결정된 드롭이 애니메이션되는 동안에는 안착하는 대상(드롭 시점의 상태)입니다. 취소된 드래그가 원래 자리로 돌아가는 동안에는 null.
booleanover가 드래그 중인 항목을 받아들이는지 여부. 아래에 아무것도 없으면 false.
DnDStatus'dragging', 그리고 오버레이가 안착하는(또는 원래 자리로 돌아가는) 동안은 'dropping'.
DragMode지금 드롭하면 일어날 일(DnDEvent.mode 참고): 예를 들어 복사하는 동안 더하기 표시를 보여 줍니다.
DropDestination | null지금 드롭하면 드래그 중인 항목이 정렬 가능한 리스트나 트리 안에서 놓일 곳. 예를 들어 트리 드래그가 들어갈 폴더에 따라 오버레이에 색을 입힐 수 있습니다. 모든 대상 밖에서는 자리의 위치이며, dropOutside: 'placeholder'(기본값)가 항목을 그곳에 남깁니다. 드롭이 항목을 어떤 리스트에도 놓지 않을 때는 null: 일반 드롭 대상 위, 항목을 거부하는 리스트 위, dropOutside: 'cancel'이나 'remove'일 때의 대상 밖. 결정된 드롭이 애니메이션되는 동안에는 안착하는 곳.
키보드
키보드 드래그(프로바이더의 keyboard): 그 키와 자유 드래그의 이동 단위.
자동 스크롤
드래그가 가장자리에 가까워질 때 컨테이너가 스크롤되는 방식(프로바이더의 autoScroll).
number컨테이너 크기에 대한 비율로 나타낸 가장자리 영역. 기본값 0.2(24–96px로 제한).
number16ms 프레임당 최대 속도(px).
number포인터가 컨테이너 가장자리에 있을 때의 속도(프레임당 px): 가장자리 영역 안쪽에서는 느리고, 가장자리를 넘으면 빨라지며, 최대 maxSpeed까지입니다. 기본값 10.
햅틱
터치 드래그가 진동하는 시점. 터치로 시작한 드래그만 진동합니다. 브라우저가 진동할 수 없거나(iOS Safari) 진동을 막는 경우(아직 페이지를 탭하지 않음)에는 아무 일도 일어나지 않습니다.
진동(navigator.vibrate): 길이(ms) 또는 켜짐/꺼짐 패턴. true는 기본 진동(10ms, refuse는 [20, 40, 20]), false나 0은 진동 없음.
type HapticPattern = boolean | number | number[]안내 문구
이벤트별 스크린 리더용 문구. 기본 문구를 쓰려면 undefined를, 아무것도 읽지 않으려면 ''를 반환하세요. context에는 정렬 가능한 리스트의 기본 문구가 읽어 주는 위치가 들어 있습니다.
문구는 프로바이더의 라이브 리전이 읽어 줍니다(이동은 드래그가 500 ms 동안 멈춘 뒤에). undefined는 이벤트를 다음 출처에 맡깁니다(정렬 위치 같은 연동 기능의 문구, 그다음 기본 문구). 집을 때 찾은 대상에 대해서는 이동을 안내하지 않습니다. 아직 아무것도 움직이지 않았기 때문입니다.
(event: DnDEvent, context: AnnouncementContext) => string | undefined드래그가 시작되었습니다.
(event: DnDEvent, context: AnnouncementContext) => string | undefined드래그가 다른 대상 위로, 또는 리스트의 다른 자리로 옮겨 갔습니다(500 ms 동안 멈춘 뒤에).
(event: DnDEndEvent, context: AnnouncementContext) => string | undefined드롭이 확정되었거나, 드래그가 모든 대상 밖에서 끝났습니다.
(event: DnDEvent, context: AnnouncementContext) => string | undefined드래그가 취소되었거나 드롭이 거부되었습니다.
(event: DnDEvent, context: AnnouncementContext) => string | undefinedonBeforeDrop이 'continue'로 응답했습니다. 드래그가 계속됩니다.
이벤트 외에 안내 문구가 다루는 내용입니다. 기본 문구와 같은 내용을 말하는 직접 만든 문구(다른 언어, 다른 표현)에 씁니다.
AnnouncedPosition정렬 가능한 리스트나 트리에서 드래그 중인 항목이 있는 자리입니다. 시작할 때는 집은 자리, 움직이는 동안에는 안착할 자리(리스트 밖에서는 없음), 끝날 때는 안착한 자리 또는 돌아간 자리입니다.
{ key: Key; label?: string; position: AnnouncedPosition }자리 맞바꾸기: 드래그 중인 항목과 맞바뀐 항목, 그리고 그 항목의 현재 자리.
number드래그가 옮기는 항목 수.
boolean드래그가 복사합니다(group.pull: 'clone', 복사 모디파이어 키).