Drag & drop is a very common effect in web page. Developers usually apply it for sorting purpose.
Days without it needs to listen mousedown
/ mousemove
/ mouseup
events and do lots of stuff to simulate drap & drop effect. Now developers could apply it in modern way.
Try to drag & drop the following example to change units sorting.
custom category 1
custom category 2
custom category 3
custom category 4
custom category 5
custom category 6
Instantiation
Developers could copy / paste the following style
、HTML
and JavaScript
into your web page and check the effect.
style
<style>
.dnd-wrap {
--row-block-size: 3em;
--icon: path('M12 2l-5.5 9h11L12 2zm0 3.84L13.93 9h-3.87L12 5.84zM17.5 13c-2.49 0-4.5 2.01-4.5 4.5s2.01 4.5 4.5 4.5 4.5-2.01 4.5-4.5-2.01-4.5-4.5-4.5zm0 7c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5zM3 21.5h8v-8H3v8zm2-6h4v4H5v-4z');
--text-color-nomal: rgba(31 31 31);
--text-color-active: rgba(31 31 31);
--text-color: var(--text-color-nomal);
--background-color-normal: rgba(255 255 255);
--background-color-active: rgba(233 240 253);
--background-color: var(--background-color-normal);
--indicator-color: rgba(83 131 236);
--drop-image-text-color: rgba(255 255 255);
--drop-image-background-color: rgba(59 103 206);
--wrap-shadow-color: rgba(0 0 0/.2);
inline-size: min(100%, 960px);
margin: 3em auto;
overflow: hidden;
border-radius: .75em;
box-shadow: 0 2px 6px var(--wrap-shadow-color);
@media (prefers-color-scheme: dark) {
--text-color-nomal: rgba(227 227 227);
--text-color-active: rgba(61 64 67);
--background-color-normal: rgba(41 42 45);
--background-color-active: rgba(147 179 242);
--indicator-color: rgba(147 179 242);
--drop-image-text-color: rgba(61 64 67);
--drop-image-background-color: rgba(147 179 242);
--wrap-shadow-color: rgba(0 0 0/.5);
}
.dnd-wrap__ul {
inline-size: 100%;
display: block;
.dnd-wrap__ul__li {
--indicator-opacity-normal: 0;
--indicator-opacity-active: 1;
--indicator-opacity: var(--indicator-opacity-normal);
--indicator-inset-block: auto 0;
position: relative;
color: var(--text-color);
background-color: var(--background-color);
font-size: 1em;
block-size: var(--row-block-size);
padding-inline: 1em;
display: flex;
gap: .5em;
align-items: center;
outline: 0 none;
will-change: background-color, color;
transition: background-color 100ms ease, color 100ms ease;
&::before {
inline-size: 1.5em;
aspect-ratio: 1/1;
content: '';
background-color: var(--text-color);
clip-path: var(--icon);
}
&:focus-within {
--text-color: var(--text-color-active);
--background-color: var(--background-color-active);
}
.dnd-wrap__ul__li__indicator {
position: absolute;
inset-inline-start: 0;
inset-block: var(--indicator-inset-block);
inline-size: 100%;
block-size: 2px;
background-color: var(--indicator-color);
display: block;
opacity: var(--indicator-opacity);
pointer-events: none;
&::before {
position: absolute;
content: '';
inset-inline-start: 0;
inset-block-start: -3px;
inline-size: 0;
block-size: 0;
border: 4px solid transparent;
border-inline-start-color: var(--indicator-color);
}
&::after {
position: absolute;
content: '';
inset-inline-end: 0;
inset-block-start: -3px;
inline-size: 0;
block-size: 0;
border: 4px solid transparent;
border-inline-end-color: var(--indicator-color);
}
}
&[data-indicator] .dnd-wrap__ul__li__indicator {
--indicator-opacity: var(--indicator-opacity-active);
z-index: 1;
}
&[data-indicator=start] {
--indicator-inset-block: 0 auto;
}
&[data-indicator=end] {
--indicator-inset-block: auto 0;
}
}
}
.dnd-drop-image {
position: absolute;
inset-inline-start: -200px;
inset-block-start: calc(var(--row-block-size) * -1);
inline-size: 200px;
color: var(--drop-image-text-color);
background-color: var(--drop-image-background-color);
block-size: var(--row-block-size);
border-radius: var(--row-block-size);
padding-inline: 1em;
box-sizing: border-box;
display: flex;
gap: .5em;
align-items: center;
p {
flex-grow: 1;
min-inline-size: 0;
}
&::before {
flex-shrink: 0;
inline-size: 1.5em;
aspect-ratio: 1/1;
content: '';
background-color: var(--drop-image-text-color);
clip-path: var(--icon);
}
}
}
</style>
HTML
<div class="dnd-wrap">
<ul class="dnd-wrap__ul">
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 1
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 2
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 3
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 4
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 5
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
<li class="dnd-wrap__ul__li" draggable="true" tabindex="0">
custom category 6
<em class="dnd-wrap__ul__li__indicator"></em>
</li>
</ul>
<div class="dnd-drop-image">
<p class="text-overflow">custom category 1</p>
</div>
</div>
JavaScript
<script type="module">
const wrap = document.querySelector('.dnd-wrap');
const ul = document.querySelector('.dnd-wrap__ul');
const dragImage = document.querySelector('.dnd-drop-image');
const dragImageP = dragImage.querySelector('p');
let units = Array.from(document.querySelectorAll('.dnd-wrap li'));
let dragInfo = {
target: '',
index: 0
};
const clearIndicator = () => {
Array.from(wrap.querySelectorAll('[data-indicator]'))
.forEach(unit => unit.toggleAttribute('data-indicator', false));
};
const onDnD = (evt) => {
const { type, target } = evt;
switch (type) {
case 'dragstart': {
const content = target.textContent.trim();
dragImageP.textContent = content;
evt.dataTransfer.setData('text/plain', content);
evt.dataTransfer.setDragImage(dragImage, 100, 40);
dragInfo.target = target;
dragInfo.index = units.indexOf(target);
break;
}
case 'dragover': {
evt.preventDefault();
evt.dataTransfer.dropEffect = 'move';
clearIndicator();
const hoverUnit = evt.target.closest('.dnd-wrap__ul__li');
if (!hoverUnit || hoverUnit === dragInfo.target) {
return;
}
const index = units.indexOf(hoverUnit);
hoverUnit.dataset.indicator = index > dragInfo.index ? 'end' : 'start';
break;
}
case 'dragend':
case 'drop': {
evt.preventDefault();
const hoverUnit = wrap.querySelector('[data-indicator]');
if (hoverUnit) {
const indicator = hoverUnit.dataset.indicator;
const index = units.indexOf(hoverUnit);
if (indicator === 'start') {
if (index === 0) {
ul.insertBefore(dragInfo.target, units[0]);
} else {
ul.insertBefore(dragInfo.target, units[index]);
}
} else {
if (index + 1 === units.length) {
ul.appendChild(dragInfo.target);
} else {
ul.insertBefore(dragInfo.target, units[index + 1]);
}
}
}
clearIndicator();
dragInfo.target.focus();
units = Array.from(document.querySelectorAll('.dnd-wrap li'));
break;
}
}
};
// events
Array.from(document.querySelectorAll('.dnd-wrap li')).forEach(
(unit) => {
unit.addEventListener('dragstart', onDnD);
unit.addEventListener('dragend', onDnD);
}
);
wrap.addEventListener('dragover', onDnD);
wrap.addEventListener('drop', onDnD);
</script>
Elements for drag
Each drag unit needs to add attribute draggable="true"
and listener dragstart
events. To make Safari active, developers also need to call event.dataTransfer.setData
to set some data.
<div class="drag-unit" draggable="true">
drag me
</div>
<script type="module">
const unit = document.querySelector('.drag-unit');
unit.addEventListener('dragstart',
(event) => {
console.log('event: dragstart');
event.dataTransfer.setData('text/plain', 'hello world');
}
);
</script>
Developers could also apply image
or exist HTML element
as customize dragImage. Once you choose exist HTML element as dragImage, you might need to arrange it away viewport to avoid annoy background you don't like.(browser will screenshot the element include background beyond it. The screenshot won't take background if element is not in viewport)
customize dragImage
<style>
.drag-image {
position: absolute;
inset-inline-start: -1000px;
inset-block-start: -1000px;
...
...
}
</style>
<div class="drag-unit" draggable="true">
drag me
</div>
<div class="drag-image">
drag image
</div>
<script type="module">
const unit = document.querySelector('.drag-unit');
const dragImage = document.querySelector('.drag-image');
unit.addEventListener('dragstart',
(event) => {
console.log('event: dragstart');
event.dataTransfer.setData('text/plain', 'hello world');
event.dataTransfer.setDragImage(dragImage, 0, 0); // element, offsetX, offsetY
}
);
</script>
Drop Zone
Developers need to listen dragover
and drop
events to make drop zone dropable. Remember call event.preventDefault()
to avoid browser default behavior.
<div class="drag-zone"></div>
<script type="module">
const dropZone = document.querySelector('.drag-zone');
const onDnD = (event) => {
const { target } = event;
event.preventDefault();
switch (type) {
case 'dragover':
// do something when dragover
break;
case 'drop':
// do something when drop
break;
}
};
dropZone.addEventListener('dragover', onDnD);
dropZone.addEventListener('drop', onDnD);
</script>
Active drag behavior in mobile device
Users need to press and hold
on drag unit almost 2 seconds
to active drag behavior in mobile browser. This is also the main different desktop & mobile browser.
Reference