Tutorial

Building an example together, step by step.

This tutorial will walk you through the basic entities of the Pragmatic drag and drop including draggables, drop targets and monitors. To understand how these pieces work together we will be creating a chess board with draggable pieces.

Starter code

Here is the starter code we'll be using throughout this guide. Notice how none of the pieces can be dragged.

Dark theme
Pawn
King
/** * @jsxRuntime classic * @jsx jsx */ import { type ReactElement } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { css, jsx } from '@emotion/react'; import king from '../../icons/king.png'; import pawn from '../../icons/pawn.png'; export type Coord = [number, number]; export type PieceRecord = { type: PieceType; location: Coord; }; export type PieceType = 'king' | 'pawn'; type PieceProps = { image: string; alt: string; }; export function isEqualCoord(c1: Coord, c2: Coord): boolean { return c1[0] === c2[0] && c1[1] === c2[1]; } export const pieceLookup: { [Key in PieceType]: () => ReactElement; } = { king: () => <King />, pawn: () => <Pawn />, }; function renderSquares(pieces: PieceRecord[]) { const squares = []; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const squareCoord: Coord = [row, col]; const piece = pieces.find((piece) => isEqualCoord(piece.location, squareCoord)); const isDark = (row + col) % 2 === 1; squares.push( <div css={squareStyles} style={{ backgroundColor: isDark ? 'lightgrey' : 'white' }}> {piece && pieceLookup[piece.type]()} </div>, ); } } return squares; } function Chessboard() { const pieces: PieceRecord[] = [ { type: 'king', location: [3, 2] }, { type: 'pawn', location: [1, 6] }, ]; return <div css={chessboardStyles}>{renderSquares(pieces)}</div>; } function Piece({ image, alt }: PieceProps) { return <img css={imageStyles} src={image} alt={alt} draggable="false" />; // draggable set to false to prevent dragging of the images } export function King() { return <Piece image={king} alt="King" />; } export function Pawn() { return <Piece image={pawn} alt="Pawn" />; } const chessboardStyles = css({ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gridTemplateRows: 'repeat(8, 1fr)', width: '500px', height: '500px', border: '3px solid lightgrey', }); const squareStyles = css({ width: '100%', height: '100%', display: 'flex', justifyContent: 'center', alignItems: 'center', }); const imageStyles = css({ width: 45, height: 45, padding: 4, borderRadius: 6, boxShadow: '1px 3px 3px rgba(9, 30, 66, 0.25),0px 0px 1px rgba(9, 30, 66, 0.31)', '&:hover': { backgroundColor: 'rgba(168, 168, 168, 0.25)', }, }); export default Chessboard;

Step 1: Making the pieces draggable

The first step to make our chess board functional is to allow the pieces to be dragged around.

Pragmatic drag and drop provides a draggable function that you attach to an element to enable the draggable behavior. When using React this is done in an effect:

function Piece({ image, alt }: PieceProps) {
    const ref = useRef(null);

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return draggable({
            element: el,
        });
    }, []);

    return <img css={imageStyles} src={image} alt={alt} ref={ref} />;
}

Our piece now behaves as follows (try dragging it around):

Dark theme
King

Although the piece can now be dragged around, it doesn't feel as though the piece is being 'picked up', as the piece stays in place while being dragged.

To make the piece fade while being dragged we can use the onDragStart and onDrop arguments within draggable to set state. We can then use this state to toggle css within the style prop to reduce the opacity.

function Piece({ image, alt }: PieceProps) {
    const ref = useRef(null);
    const [dragging, setDragging] = useState<boolean>(false); // NEW

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return draggable({
            element: el,
            onDragStart: () => setDragging(true), // NEW
            onDrop: () => setDragging(false), // NEW
        });
    }, []);

    return (
        <img
            css={[dragging && hidePieceStyles, imageStyles]} // toggling css using state to hide the piece
            src={image}
            alt={alt}
            ref={ref}
        />
    );
}

Now the piece fades when we drag it, making it feel like the piece is being 'picked up'. 🥳

Dark theme
King

Now let's add it to the board!

Dark theme
Pawn
King

To see the full draggable documentation see this page.

Step 2: Making the squares drop targets

Now that we have draggable pieces we want the squares on the board to act as areas that can be 'dropped' onto. For this we will use the dropTargetForElements function from Pragmatic drag and drop.

Drop targets are elements that a draggable element can be dropped on.

Creating a drop target follows the same technique as for draggable. Let's abstract out the board's squares, which were previously divs, into their own component.

function Square({ location, children }: SquareProps) {
    const ref = useRef(null);
    const [isDraggedOver, setIsDraggedOver] = useState(false);

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return dropTargetForElements({
            element: el,
            onDragEnter: () => setIsDraggedOver(true),
            onDragLeave: () => setIsDraggedOver(false),
            onDrop: () => setIsDraggedOver(false),
        });
    }, []);

    const isDark = (location[0] + location[1]) % 2 === 1;

    return (
        <div css={squareStyles} style={{ backgroundColor: getColor(isDraggedOver, isDark) }} ref={ref}>
            {children}
        </div>
    );
}

Similar to the draggable piece component, we set state on the component based on the dragging behavior.

We then use this state to set the color of the square using the getColor function:

function getColor(isDraggedOver: boolean, isDark: boolean): string {
    if (isDraggedOver) {
        return 'skyblue';
    }
    return isDark ? 'lightgrey' : 'white';
}

The squares now highlight when dragged over!

Dark theme
Pawn
King

To take this a step further we can color the square green when a piece is eligible to be dropped onto and red when it is not.

To achieve this we first use the getInitialData argument on draggable to surface the piece type and starting location of the dragging piece.

function Piece({ location, pieceType, image, alt }: PieceProps) {
    const ref = useRef(null);
    const [dragging, setDragging] = useState<boolean>(false);

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return draggable({
            element: el,
            getInitialData: () => ({ location, pieceType }), // NEW
            onDragStart: () => setDragging(true),
            onDrop: () => setDragging(false),
        });
    }, [location, pieceType]);

    /*...*/
}

We then need to consume this data at the drop targets.

You can see below that the drop target can now access to the draggable element's location and piece type that was surfaced from the draggable. We've also introduced a new canMove function which determines whether a piece can move to a square based on the start and end location, the piece type and whether there is already a piece on that square.

What is important to note is that when using Typescript the type of the data is not carried over from the draggable to the drop target's source. Therefore we need to call the type guarding functions isCoord and isPieceType before canMove can be called.

type HoveredState = 'idle' | 'validMove' | 'invalidMove';

function Square({ pieces, location, children }: SquareProps) {
    const ref = useRef(null);
    const [state, setState] = useState<HoveredState>('idle');

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return dropTargetForElements({
            element: el,
            onDragEnter: ({ source }) => {
                // source is the piece being dragged over the drop target
                if (
                    // type guards
                    !isCoord(source.data.location) ||
                    !isPieceType(source.data.pieceType)
                ) {
                    return;
                }

                if (canMove(source.data.location, location, source.data.pieceType, pieces)) {
                    setState('validMove');
                } else {
                    setState('invalidMove');
                }
            },
            onDragLeave: () => setState('idle'),
            onDrop: () => setState('idle'),
        });
    }, [location, pieces]);

    /*...*/
}

The new state is then used to set the color of the square as before.

function getColor(state: HoveredState, isDark: boolean): string {
    if (state === 'validMove') {
        return 'lightgreen';
    } else if (state === 'invalidMove') {
        return 'pink';
    }
    return isDark ? 'lightgrey' : 'white';
}

When put all together, the squares now highlight if a move is valid when hovered over.

Dark theme
Pawn
King

We can also make use of the data we attached to the draggable to prevent interractions with the square it is being dragged from. This makes use of the canDrop argument on dropTargetForElements.

return dropTargetForElements({
    element: el,
    canDrop: ({ source }) => {
        // NEW
        if (!isCoord(source.data.location)) {
            return false;
        }

        return !isEqualCoord(source.data.location, location);
    },
    // ...the rest of our dropTargetForElements arguments
});

Now we can see that the square the piece is currently in does not change color when hovered over and cannot be dropped onto. This works by disabling the drop target functionality when canDrop returns false.

Dark theme
Pawn
King

See this page, for the full documentation on drop targets.

Step 3: Moving the pieces

Finally let's allow the pieces to move squares when dropped. To achieve this we will use a monitorForElements from Pragmatic drag and drop.

Monitors allow you to observe drag and drop interactions from anywhere in your codebase. This allows them to recieve draggable and drop target data and perform operations without needing state to be passed from components.

Therefore we can place a monitor within a useEffect at the top level of our chessboard and listen for when pieces are dropped into squares.

To achieve this we first need to surface the location of the squares within the drop target, as we did for the draggable pieces in the previous step:

function Square({ pieces, location, children }: SquareProps) {
    const ref = useRef(null);
    const [state, setState] = useState<HoveredState>('idle');

    useEffect(() => {
        const el = ref.current;
        invariant(el);

        return dropTargetForElements({
            element: el,
            getData: () => ({ location }), // NEW

            /*...*/
        });
    });

    /*...*/
}

We then add a monitor to the chessboard. Much of this logic mirrors the logic explained above for coloring squares.

function Chessboard() {
    const [pieces, setPieces] = useState<PieceRecord[]>([
        { type: 'king', location: [3, 2] },
        { type: 'pawn', location: [1, 6] },
    ]);

    useEffect(() => {
        return monitorForElements({
            onDrop({ source, location }) {
                const destination = location.current.dropTargets[0];
                if (!destination) {
                    // if dropped outside of any drop targets
                    return;
                }
                const destinationLocation = destination.data.location;
                const sourceLocation = source.data.location;
                const pieceType = source.data.pieceType;

                if (
                    // type guarding
                    !isCoord(destinationLocation) ||
                    !isCoord(sourceLocation) ||
                    !isPieceType(pieceType)
                ) {
                    return;
                }

                const piece = pieces.find((p) => isEqualCoord(p.location, sourceLocation));
                const restOfPieces = pieces.filter((p) => p !== piece);

                if (
                    canMove(sourceLocation, destinationLocation, pieceType, pieces) &&
                    piece !== undefined
                ) {
                    // moving the piece!
                    setPieces([{ type: piece.type, location: destinationLocation }, ...restOfPieces]);
                }
            },
        });
    }, [pieces]);

    /*...*/
}

And voila! We now have a chessboard with moving pieces. Go ahead and try dragging the pieces around.

You can also have a look through the code for more detail on the typing, type guarding and other details we skimmed over in writing.

Dark theme
Pawn
King
/** * @jsxRuntime classic * @jsx jsx */ import { type ReactElement, useEffect, useState } from 'react'; // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766 import { css, jsx } from '@emotion/react'; import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter'; import { King, Pawn } from './draggable-piece-with-data'; import Square from './square-with-data'; export type Coord = [number, number]; export type PieceRecord = { type: PieceType; location: Coord; }; export type PieceType = 'king' | 'pawn'; export function isCoord(token: unknown): token is Coord { return ( Array.isArray(token) && token.length === 2 && token.every((val) => typeof val === 'number') ); } const pieceTypes: PieceType[] = ['king', 'pawn']; export function isPieceType(value: unknown): value is PieceType { return typeof value === 'string' && pieceTypes.includes(value as PieceType); } export function isEqualCoord(c1: Coord, c2: Coord): boolean { return c1[0] === c2[0] && c1[1] === c2[1]; } export const pieceLookup: { [Key in PieceType]: (location: [number, number]) => ReactElement; } = { king: (location) => <King location={location} />, pawn: (location) => <Pawn location={location} />, }; export function canMove( start: Coord, destination: Coord, pieceType: PieceType, pieces: PieceRecord[], ) { const rowDist = Math.abs(start[0] - destination[0]); const colDist = Math.abs(start[1] - destination[1]); if (pieces.find((piece) => isEqualCoord(piece.location, destination))) { return false; } switch (pieceType) { case 'king': return [0, 1].includes(rowDist) && [0, 1].includes(colDist); case 'pawn': return colDist === 0 && start[0] - destination[0] === -1; default: return false; } } function renderSquares(pieces: PieceRecord[]) { const squares = []; for (let row = 0; row < 8; row++) { for (let col = 0; col < 8; col++) { const squareCoord: Coord = [row, col]; const piece = pieces.find((piece) => isEqualCoord(piece.location, squareCoord)); squares.push( <Square pieces={pieces} location={squareCoord}> {piece && pieceLookup[piece.type](squareCoord)} </Square>, ); } } return squares; } function Chessboard() { const [pieces, setPieces] = useState<PieceRecord[]>([ { type: 'king', location: [3, 2] }, { type: 'pawn', location: [1, 6] }, ]); useEffect(() => { return monitorForElements({ onDrop({ source, location }) { const destination = location.current.dropTargets[0]; if (!destination) { return; } const destinationLocation = destination.data.location; const sourceLocation = source.data.location; const pieceType = source.data.pieceType; if (!isCoord(destinationLocation) || !isCoord(sourceLocation) || !isPieceType(pieceType)) { return; } const piece = pieces.find((p) => isEqualCoord(p.location, sourceLocation)); const restOfPieces = pieces.filter((p) => p !== piece); if ( canMove(sourceLocation, destinationLocation, pieceType, pieces) && piece !== undefined ) { setPieces([{ type: piece.type, location: destinationLocation }, ...restOfPieces]); } }, }); }, [pieces]); return <div css={chessboardStyles}>{renderSquares(pieces)}</div>; } const chessboardStyles = css({ display: 'grid', gridTemplateColumns: 'repeat(8, 1fr)', gridTemplateRows: 'repeat(8, 1fr)', width: '500px', height: '500px', border: '3px solid lightgrey', }); export default Chessboard;

See our monitor page, for the full documentation on monitors.

Now it's your turn

You're now ready to start building your own projects with Pragmatic drag and drop.

For more examples see our example page, or to see how to drag and drop files with Pragmatic drag and drop see our external adapter page.


Was this page helpful?
We use this feedback to improve our documentation.