Erratic Generator

Creative Coding and Design

About

React Draggable With CSS Transition

Create a draggable element that also animates.

React Draggable component and CSS transition

In this post, I would like to document my process of learning to use react-draggable component. The basic usage is quite simple - you only need to wrap your element with a Draggable component. To learn how, you can refer to the original repo or read a write up by Victoria Lo on Medium.

What I wanted to do was a little different in that I not only wanted to drag elements but also attach CSS transition animation to them, and that's what I want to cover in this post.

Setup

I will use create-react-app for simplicity.

$ npx create-react-app react-drag-test

And then, install dependencies. I am also using Emotion css-in-js library for styling because I will be passing a prop to Emotion's styled-component later.

$ npm i react-draggable @emotion/react @emotion/styled

When you have everything ready, clean up what's in App.js and start your local dev server.

$ npm start

Basic Usage

The basic usage of react-draggable is explained in the links I posted above so I won't spend too much time on it. I am creating a simple div as an Emotion's styled component. You only need to wrap your div with Draggable component. Note that the draggable can only have a single child so if you have multiple elements, make sure you create another level of hierarchy with either div or span.

import Draggable from "react-draggable";
import styled from "@emotion/styled";

const Box = styled.div`
  width: 200px;
  height: 200px;
  border: 1px solid black;
  cursor: move;
`;

function App() {
  return (
    <Draggable>
      <Box>Drag me.</Box>
    </Draggable>
  );
}

export default App;

Now, you can drag your box! It's that easy. If you look at your browser's dev tool, you will notice that the Draggable component does not create any new DOM element, instead, it adds CSS transform property to its immediate child to translate from its original position.

Multiple Draggables

What if you have multiple elements, for example, a list of elements that you want to drag? You can wrap each element with Draggable component and supply a key prop.

const List = styled.ul`
  margin: 0 auto;
  padding: 0;
  width: 400px;
  background-color: gray;
  display: grid;
  grid-template-columns: 1fr 1fr;
  grid-template-rows: 1fr 1fr;
  list-style-type: none;
`;

const Box = styled.li`
  height: 200px;
  border: 1px solid black;
  background: yellow;
  cursor: move;
`;

const list = [`First`, `Second`, `Third`, `Fourth`];

function App() {
  return (
    <List>
      {list.map((item, idx) => {
        return (
          <Draggable key={idx}>
            <Box>{item}</Box>
          </Draggable>
        );
      })}
    </List>
  );
}

export default App;

Event Handlers

Let's come back to a single draggable item, and take a look at event handlers. You can log what types of data are being passed on, including the basic x and y, screenX and screenY, and more.

function App() {
  const eventLogger = (e, data) => {
    console.log("Event: ", e);
    console.log("Data: ", data);
  };

  return (
    <Draggable onStart={eventLogger} onDrag={eventLogger} onStop={eventLogger}>
      <Box>Drag me</Box>
    </Draggable>
  );
}

export default App;

Controlled element

You can also pass the position prop and the position will be controlled. You can still drag the element but the moment you release the mouse, it will snap back because it is controlled by the value set in position. If you want to completely disable the dragging behavior, you can pass disabled prop and set its value to false.

<Draggable position={{ x: 0, y: 0 }}>
  <Box>Drag me</Box>
</Draggable>

I wanted it to have a nice transition animation when the element returns instead of jumping back. Draggable adds transform: translate(x, y) to the CSS of its immediate child and the translate value is only an offset value, so, if we can set the values back to (0, 0), we can easily return it back to its original position. My first thought was adding a CSS transition to transform property.

const Box = styled.div`
  /* basic styling here */

  transition: transform 0.3s;
`;

The code above sort of worked but the transition was also activated when I was dragging the element which made using it very difficult and annoying.

Instead of setting the position prop directly, I decided to set it through the onDrag event handler. To store and update position value, I added a position state.

import { useState } from 'react'
import Draggable from "react-draggable";
import styled from "@emotion/styled";

const Box = styled.div`
  width: 200px;
  height: 200px;
  border: 1px solid black;
  background: yellow;
  cursor: move;
  transition: transform 0.3s;
`;

function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 })

  const handleDrag = (e, data) => {
    setPosition({ x: data.x, y: data.y })    
  };

  return (
    <Draggable position={position} onDrag={handleDrag}>
      <Box>Drag me</Box>
    </Draggable>
  );
}

export default App;

It's the same as the basic example earlier, but now I have the position prop and I can update its values manually. I want to disable transition when I am dragging the element, and bring the transition back when I am not dragging it. Basically, I need to toggle the CSS transition state through the event handlers. To toggle the transition, I need to introduce another state isControlled.

Putting It All Together

Below is the complete code example. The changes here are the introduction of props in the styled component. You can pass any props like in a regular React component and use them in your styling. I also added handleStart and handleStop to toggle the isControlled value as well as resetting the position value.

import { useState } from "react";
import Draggable from "react-draggable";
import styled from "@emotion/styled";

const Box = styled.div`
  width: 200px;
  height: 200px;
  border: 1px solid black;
  background: yellow;
  cursor: move;
  transition: ${props => props.isControlled ? `transform 0.3s` : `none`};
`;

function App() {
  const [position, setPosition] = useState({ x: 0, y: 0 });
  const [isControlled, setIsControlled] = useState(true);

  const handleStart = () => {
    setIsControlled(false)
  }
  const handleDrag = (e, data) => {
    setPosition({ x: data.x, y: data.y });
  };
  const handleStop = () => {
    setIsControlled(true)
    setPosition({x:0, y:0})
  }

  return (
    <Draggable
      position={position}
      onStart={handleStart}
      onDrag={handleDrag}
      onStop={handleStop}
    >
      <Box isControlled={isControlled}>Drag me</Box>
    </Draggable>
  );
}

export default App;

Wrap-up

Starting with this basic example, I was able to combine both dragging and animation at any point in my program. There are quite a few more props you can use in a Draggable component so I suggest that you check out the original repo and their example. I found defaultPosition and bounds to be very helpful.

Lastly, here is a tip if you want to drag an image element. Set <img draggable={false} /> otherwise, it will try to drag the image out of your browser. That draggable attribute is not part of React or react-draggable package. It is a pure HTML attribute.

I hope this post was helpful for you, and if you have a suggestion to make this better, please let me know.

References

If you like my contents, please consider supporting my website. It helps me create more contents like this. Thank you!

Buy Me a Coffee at ko-fi.com
Copyright 2020-2021 • ErraticGenerator.com