← Back toDrag and drop in React

Create your own ghost element

Written byPhuoc Nguyen
Created
27 Nov, 2023
Tags
ghost element, HTML 5 drag and drop
In our previous post, we learned how to customize the appearance of a ghost element during an HTML5 drag and drop operation. We achieved this by using the `setDragImage()` API, which allows you to specify a new image for the ghost element at any point during the drag and drop process.
To use `setDragImage()`, you'll need to create a new element that will be used as the updated ghost element. Then, you can pass this element into `setDragImage()` along with an x and y offset that determines where the ghost element should be positioned relative to the cursor.
Here's a code snippet to remind you of what we've done:
ts
const handleDragStart = (e) => {
const dragEle = e.target;

const ghostEle = dragEle.cloneNode(true);
ghostEle.classList.add('dragging');
document.body.appendChild(ghostEle);

const nodeRect = dragEle.getBoundingClientRect();
e.dataTransfer.setDragImage(
ghostEle,
e.clientX - nodeRect.left,
e.clientY - nodeRect.top
);
};
This approach works great if you want the ghost element to have the same structure as the original element. In that case, we use the `cloneNode()` function to create a copy of the target element being dragged.
But what if you want to create your own unique ghost element? In that case, you can use the normal DOM APIs to create the ghost element yourself, rather than cloning the original element.
Here's an example:
ts
const handleDragStart = (e) => {
const ghostEle = document.createElement('div');
// Build the ghost element ...
document.body.appendChild(ghostEle);
};
The approach we've been using may not work well if the ghost element has a complicated markup or if the markup depends on certain conditions that are determined by internal states.
To address this issue, we recommend using React JSX to render the ghost element instead of building it from scratch. This involves hiding the default ghost element and replacing it with our own custom ghost element.

Hiding the default ghost element

To make the default ghost element invisible, we can add a custom CSS class to the `setDragImage()` API. This modification allows us to create our own custom ghost element. Here's how we can tweak the `dragstart` event handler to achieve this.
ts
const handleDragStart = (e) => {
const dragEle = e.target;

const dragImage = document.createElement('div');
dragImage.classList.add('dragging');
dragImage.classList.add('dragging--hidden');
document.body.appendChild(dragImage);

e.dataTransfer.setDragImage(dragImage, 0, 0);
}
In this example, the `dragImage` represents the ghost element, and we've added the `dragging--hidden` class to it. This class is responsible for making the ghost element invisible during the drag and drop operation.
To make the ghost element invisible, the class sets the `opacity` to 0, which makes the element completely transparent, and sets the `visibility` to `hidden`, which hides the element from view. By adding this class to the ghost element, we can ensure that it remains hidden while still being used as a reference point for positioning during the drag and drop operation.
css
.dragging--hidden {
opacity: 0;
visiblity: hidden;
}

Creating a custom ghost element

Our custom ghost element should only appear when users are actively dragging the target element. To achieve this, we first need to use an internal state to check whether the target element is being dragged or not. By default, this state is initialized to `false`, indicating that the element is not currently being dragged.
ts
const [isDragging, setIsDragging] = React.useState(false);
When the `dragstart` event happens, we update the state by calling `setIsDragging(true)` to indicate that dragging has begun. This triggers a re-render of the component with the updated state value.
Furthermore, we attach a `dragend` event listener to remove the default ghost element and reset the `isDragging` state once dragging is complete. This ensures that subsequent drag operations will start with a fresh state and prevent any unwanted behavior.
ts
const handleDragStart = (e) => {
const dragEle = e.target;
setIsDragging(true);

const handleDragEnd = () => {
dragImage.remove();
setIsDragging(false);
};

dragEle.addEventListener('dragend', handleDragEnd);
};
Our custom ghost element will only appear when the `isDragging` state is set to `true`.
tsx
{
isDragging && (
<div
className="dragging"
style={{
transform: `translate(${dx}px, ${dy}px)`,
height: `${dragSize.h}px`,
width: `${dragSize.w}px`,
}}
>
Drop me
</div>
)
}
No need to worry about the additional styles of the ghost element just yet. We'll cover those in detail soon. For now, let's start by positioning the ghost element absolutely within its container.
To make the ghost element follow the cursor, we first set its position to `absolute`. This lets us move the element around without disturbing other elements on the page.
In our code, we set the `top` and `left` properties of the `.dragging` class to 0. This positions the element at the top left of its container.
css
.dragging {
position: absolute;
top: 0;
left: 0;
}
Next, we utilize JavaScript to calculate the distance that the target element has moved since the drag operation began. The `dx` and `dy` values indicate the amount by which the target element has been displaced. We apply the same technique when enabling an element to be draggable. These two properties are initially set to 0, forming part of the internal state.
These values enable us to dynamically update the `transform` property of our `.dragging` class, resulting in a smooth and seamless user experience.
ts
const [{ dx, dy }, setOffset] = React.useState({
dx: 0,
dy: 0,
});
In this example, we're going to calculate the starting position of the element when it's dragged across the screen. To do this, we subtract the position of the target element's top-left corner from the position of the cursor when dragging begins. This ensures that the element follows the cursor smoothly and precisely.
To get the position of the target element, we use `getBoundingClientRect()`. This handy method retrieves the element's bounding rectangle relative to the viewport. We also get the bounding rectangle of its parent container using the same method, and subtract it from our initial calculation. This ensures that our starting position is always relative to the parent container.
Here's how we can modify the `dragstart` event handler:
ts
const handleDragStart = (e) => {
const dragEle = e.target;

const parentRect = dragEle.parentElement.getBoundingClientRect();
let dx = nodeRect.left - parentRect.left;
let dy = nodeRect.top - parentRect.top;
const startPos = {
x: e.clientX - dx,
y: e.clientY - dy,
};
};
After we set our starting position, we attach a `drag` event listener to keep track of the cursor's position as we drag it. The `handleDrag()` function calculates new values for `dx` and `dy` based on these changes, and then updates their values in state using `setOffset()`. This triggers a re-render of the component with updated styles for our custom ghost element, which now moves along with the cursor.
ts
const handleDrag = (e) => {
const dx = e.clientX - startPos.x;
const dy = e.clientY - startPos.y;
setOffset({ dx, dy });
};

dragEle.addEventListener('drag', handleDrag);
Next, we need to make sure that our customized ghost element has the right dimensions and matches its original counterpart when we drag and drop it.
To achieve this, we use the `dragSize` state to determine the size of our custom ghost element during drag and drop operations. By default, `dragSize` is an object with a width and height of 0.
ts
const [dragSize, setDragSize] = React.useState({
w: 0,
h: 0,
});
When the `dragstart` event happens, we update the `dragSize` state by using `getBoundingClientRect()` to get the width and height of the target element. This way, the custom ghost element we create will have the same dimensions as the original element being dragged.
ts
const handleDragStart = (e) => {
const dragEle = e.target;
const nodeRect = dragEle.getBoundingClientRect();
setDragSize({
w: nodeRect.width,
h: nodeRect.height,
});
};
Next, we take the state value and apply it to the `height` and `width` of our custom ghost element using inline styles in our JSX code.
tsx
<div
className="dragging"
style={{
height: `${dragSize.h}px`,
width: `${dragSize.w}px`,
}}
>
Drop me
</div>
When creating a custom ghost element for drag and drop operations, it's crucial to ensure that it doesn't interfere with other elements on the page. One easy way to achieve this is by giving the ghost element a negative value for the `z-index` property.
By default, elements are positioned on the same plane as other elements on the page, with a `z-index` value of 0. However, we can position our ghost element behind all other elements on the page by setting a negative value for the `z-index`. This guarantees that it doesn't obstruct any content or disrupt drag-drop interactions.
To set the `z-index` of our custom ghost element in the example code, we can simply use inline styles and assign it a value of -1.
tsx
<div
className="dragging"
style={{
zIndex: -1,
}}
>
Drop me
</div>
Take a look at the demo below:

Conclusion

In conclusion, incorporating a custom ghost element for drag and drop functionality can significantly enhance the user experience of your web application. By using CSS to style the element and inline styles to track its position relative to the cursor during dragging, we can create a seamless and intuitive drag and drop experience for users.
However, it's important to ensure that our custom element doesn't interfere with other elements on the page. To avoid this, we can set a negative value for the `z-index` property of our ghost element, positioning it behind all other elements on the page during drag and drop operations. This ensures that it doesn't obscure any content or interfere with user interactions.
By following these best practices and leveraging HTML 5's built-in drag and drop API, we can create powerful and user-friendly interfaces that allow users to effortlessly interact with our applications.
If you found this post helpful, please consider giving the repository a star on GitHub or sharing the post on your favorite social networks 😍. Your support would mean a lot to me!

Questions? 🙋

Do you have any questions about front-end development? If so, feel free to create a new issue on GitHub using the button below. I'm happy to help with any topic you'd like to learn more about, even beyond what's covered in this post.
While I have a long list of upcoming topics, I'm always eager to prioritize your questions and ideas for future content. Let's learn and grow together! Sharing knowledge is the best way to elevate ourselves 🥷.
Ask me questions

Recent posts ⚡

Newsletter 🔔

If you're into front-end technologies and you want to see more of the content I'm creating, then you might want to consider subscribing to my newsletter.
By subscribing, you'll be the first to know about new articles, products, and exclusive promotions.
Don't worry, I won't spam you. And if you ever change your mind, you can unsubscribe at any time.
Phước Nguyễn