Drag previews

How to control what is under the users pointer while a drag is occurring

A drag preview is the thing that a user drags around during a drag operation. We have a number of supported techniques for controlling what the drag preview looks like.

Native drag previews

We recommend using native drag previews where possible as they have great performance characteristics (they are not rendered on the main thread) and they can be dragged between applications

Browsers have built in "native" mechanisms for rendering a drag preview

There are a few techniques you can use to control what a native drag preview will look like:

Approach 1: Use a custom native drag preview

You can ask the browser to take a photo of another visible element on the page and use that as the drag preview. There are some design constraints when leveraging native drag previews.

There are lots of platform gotchas when working with custom native drag previews. We recommend using our setCustomNativeDragPreview() as it makes it safe and easy to work with custom native drag previews.

Mounting a new element with setCustomNativeDragPreview

You can use setCustomNativeDragPreview to mount a new element to the page to be used as the drag preview. setCustomNativeDragPreview will call your cleanup function to remove the preview element from the page after the browser has taken a photo of the element. setCustomNativeDragPreview adds the container Element to the document.body and will remove the container Element after your cleanup function is called.

setCustomNativeDragPreview has been designed to work with any view abstraction.

Note: you are welcome to use the onGenerateDragPreview | nativeSetDragImage, (opens new window) API directly. However, we recommend you use setCustomNativeDragPreview as it covers over a number of gotchas.

Usage example: react portals

This technique requires your component to be re-rendered, but maintains the current react context

type State =
    | {
            type: 'idle';
      }
    | {
            type: 'preview';
            container: HTMLElement;
      };

function Item() {
    const [state, setState] = useState<State>({ type: 'idle' });
    const ref = useRef<HTMLDivElement | null>(null);

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

        return draggable({
            element: ref.current,
            onGenerateDragPreview({ nativeSetDragImage }) {
                setCustomNativeDragPreview({
                    render({ container }) {
                        // Cause a `react` re-render to create your portal synchronously
                        setState({ type: 'preview', container });
                        // In our cleanup function: cause a `react` re-render to create remove your portal
                        // Note: you can also remove the portal in `onDragStart`,
                        // which is when the cleanup function is called
                        return () => setState({ type: 'idle' });
                    },
                    nativeSetDragImage,
                });
            },
        });
    }, []);

    return (
        <>
            <div ref={ref}>Drag Me</div>
            {state.type === 'preview' ? ReactDOM.createPortal(<Preview />, state.container) : null}
        </>
    );
}

Usage example: A new react application

This technique requires no re-rendering of your component, but does not maintain the current react context

import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage }) => {
        setCustomNativeDragPreview({
            render({ container }) {
                ReactDOM.render(<Preview item={item} />, container);
                return function cleanup() {
                    ReactDOM.unmountComponentAtNode(container);
                };
            },
            nativeSetDragImage,
        });
    },
});

Usage example: plain JavaScript

import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage }) => {
        setCustomNativeDragPreview({
            render({ container }) {
                // Create our preview element
                const preview = document.createElement('div');

                // Populate and style the preview element however you like
                preview.textContent = 'My Preview';
                Object.assign(preview.style, {
                    padding: '20px',
                    backgroundColor: 'lightpink',
                });

                // put the "preview" element into the container element
                container.appendChild(preview);
            },
            nativeSetDragImage,
        });
    },
});

Positioning the drag preview

You can control where the custom native drag preview is placed by using the getOffset() argument.

You can return an {x: number, y: number} object from getOffset() which will control where the native drag preview is rendered relative to the users pointer. {x: 0, y: 0} represents having the users pointer user the top left corner of the drag preview.

For clarity:

const rect = container.getBoundingClientRect()

  • {x: 0, y: 0} → top left of the container will be under the users pointer (default)
  • {x: rect.width, y: 0} top right of the container will be under the users pointer
  • {x: rect.width, y: rect.height} bottom right of the container will be under the users pointer
  • {x: 0, y: rect.height} bottom left of the container will be under the users pointer
type GetOffsetFn = (args: { container: HTMLElement }) => {
    x: number;
    y: number;
};

Notes:

  • GetOffsetFn needs to return x and y as numbers as that is what the platform requires
  • You cannot use negative values (not supported by browsers). If you want to push the drag preview away from the users pointer, use pointerOutsideOfPreview (see below)
  • The max offset value for an axis is the border-box. Values greater than the border-box get trimmed to be the border-box value
  • getOffset is called in the next microtask, (opens new window) after setCustomNativeDragPreview:render. This helps ensure that the drag preview element has finished rendering into the container before getOffset is called. Some frameworks like react@18 won't render the element to be used for the drag preview into the container until the next microtask.

{x: rect.width + 1, y: rect.height + 1} effectively becomes {x: rect.width, y: rect.height}.

import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage }) => {
        setCustomNativeDragPreview({
            // place the (near) top middle of the `container` under the users pointer
            getOffset: () => {
                const rect = container.getBoundingClientRect();
                return { x: rect.width / 2, y: 16 };
            },
            render({ container }) {
                ReactDOM.render(<Preview item={item} />, container);
                return function cleanup() {
                    ReactDOM.unmountComponentAtNode(container);
                };
            },
            nativeSetDragImage,
        });
    },
});

We have getOffset() helpers for setCustomnativeDragPreview():

  1. centerUnderPointer: centers the custom native drag preview under the users cursor
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
import { centerUnderPointer } from '@atlaskit/pragmatic-drag-and-drop/element/center-under-pointer';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage }) => {
        setCustomNativeDragPreview({
            getOffset: centerUnderPointer,
            render({ container }) {
                /* ... */
            },
            nativeSetDragImage,
        });
    },
});
  1. pointerOutsideOfPreview: a cross browser mechanism to push the drag preview in front of the users pointer.
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
import { pointerOutsideOfPreview } from '@atlaskit/pragmatic-drag-and-drop/element/pointer-outside-of-preview';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage }) => {
        setCustomNativeDragPreview({
            // `x` and `y` can be any CSS value
            getOffset: pointerOutsideOfPreview({
                x: '8px',
                y: 'calc(var(--grid) * 2)',
            }),
            render({ container }) {
                /* ... */
            },
            nativeSetDragImage,
        });
    },
});

When in left to right (ltr) languages, the drag preview is pushed forward, to the right of the users pointer. For right to left (rtl) languages, the drag preview is also pushed forward, and the preview will be on the left hand side of the users pointer. The direction (based on the dir attribute) will be looked up on the container element; so you can control the dir by setting it on the body; or on the container element itself.

onGenerateDragPreview({ nativeSetDragImage, source, location }) {
    setCustomNativeDragPreview({
        nativeSetDragImage,
        getOffset: pointerOutsideOfPreview({
            x: token('space.200'),
            y: token('space.100'),
        }),
        render({ container }) {
            // Forcing the direction for the container.
            container.dir = 'ltr';

            // By default, the `dir` inherited on `container` (which is a child of `body`) will be used.
        },
    });
},

If you are using css variables inside of your getOffset() you need to be sure your css variables are available at the <body> element, as the container is temporarily mounted as a child of <body>.

On iOS, iPad and Android pointerOutsideOfPreview will center the drag preview under the users pointer. Browsers on iOS, iPadOS and Android put the center of the drag preview under the users pointer during the drag, even when we try to "push" the drag preview away from the pointer.

  1. preserveOffsetOnSource: applies the initial cursor offset to the custom native drag preview for a seamless experience
import { setCustomNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview';
import { preserveOffsetOnSource } from '@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source';

draggable({
    element: myElement,
    onGenerateDragPreview: ({ nativeSetDragImage, location, source }) => {
        setCustomNativeDragPreview({
            getOffset: preserveOffsetOnSource({
                element: source.element,
                input: location.current.input,
            }),
            render({ container }) {
                /* ... */
            },
            nativeSetDragImage,
        });
    },
});

Notes:

  • This helper works best when the rendered preview has the same dimensions as the dragged element
  • On Android the center of the drag preview is always under the users pointer (platform limitation)
  • On iOS and iPadOS, the drag preview location will initially match the source, but during the drag the native preview will perform an animated slide so that it is centered on the users pointer. The centering of the drag preview during the drag on the users pointer is iOS and iPad platform behaviour.

Gotcha: CSS transforms

When creating custom drag preview element with setCustomNativeDragPreview, there is mixed support for applying CSS transforms to the drag preview element.

ScaleRotateTranslate (avoid)
Chrome (114.0)
Firefox (115.0)
Safari (16.5.2) (broken) (broken) (broken)

Avoid using translate for positioning a drag preview (or pushing it away from the cursor). Please use setCustomNativeDragPreview > getOffset for that (see above)

You can use CSS transforms as a progressive enhancement. For Chrome and Firefox you can use CSS transforms, but for Safari you cannot. You will need do a browser check for Safari, and only add CSS transforms to your preview element when the browser is not Safari.

const isSafari: boolean =
    navigator.userAgent.includes('AppleWebKit') && !navigator.userAgent.includes('Chrome');

const transformStyles = css({
    transform: 'scale(4deg)',
});

function Preview() {
    return <div css={isSafari ? transformStyles : undefined}>Drag preview</div>;
}

Approach 2: Change the appearance of a draggable

This approach has the best performance characteristics, but is subject to a number of gotchas. For most consumers we recommend using setCustomNativeDragPreview

If you do nothing, then the browser will use a picture of the draggable element as the drag preview. By leveraging event timings you can control the appearance of the drag preview that the browser generates:

  1. in onGenerateDragPreview make whatever visual changes you want to the draggable element and those changes will be captured in the drag preview

  2. in onDragStart:

    2a. revert changes of step 1. The user will never see the draggable element with the styles applied in onGenerateDragPreview due to paint timings

    2b. apply visual changes to the draggable element to make it clear to the user what element is being dragged

  3. in onDrop remove any visual changes you applied to the draggable element during the drag

There are a few constraints imposed by browsers that you need to follow if you want to use this technique:

  • Your draggable needs to be completely visible and unobfiscated at the start of the drag. This can involve insuring that your draggable is not cut off by scroll (see scrollJustEnoughIntoView), and has no layers currently on top of the draggable (for example, you might need to close some popups)
  • The users pointer still needs to be over the draggable after the changes you make to the draggable element in onGenerateDragPreview. Generally this means that you should not be changing the dimensions of the draggable element.
  • Avoid CSS transform on your draggable. In Safari, CSS transforms that impact a draggable can mess up native drag previews.

Non-native custom drag previews

In some situations, you might want to completely disable the native drag preview and render your own drag preview. The advantage of this technique is that you can update the drag preview during a drag. The downsides of this approach is that it is not as fast, and you cannot drag the non-native drag preview outside of a browser window.

To use this technique:

  1. disable the native drag preview
import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';

draggable({
    element: myElement,
    onGenerateDragPreview({ nativeSetDragPreview }) {
        disableNativeDragPreview({ nativeSetDragPreview });
    },
});

This technique renders a 1x1 transparent image as the native drag preview. There are a few alternative techniques for hiding the drag preview, but this technique yielded the best results across many browsers and devices.

  1. render your own element in onDragStart (ideally in a portal), and under the user's pointer (you can use location.initial.input to get the users initial position)
  2. move the new element around in response to onDrag events (use location.current.input to get the users current pointer position)
  3. remove the new element in onDrop

If you are doing this technique, you will likely want to use the preventUnhandled utility. Using that addon will prevent the strange situation where when the user does not drop on a drop target there is a fairly large pause before the drop event. This is because the browser does a drop animation when the user does not drop on a drop target; a "return home" animation. Because you have hidden the native drag preview, the user won't see this return home drop animation, but will experience a delay. Using the preventUnhandled utility ensures that the return home drop animation won't run

No drag preview

For some experiences you might not want any drag preview (for example, resizing). All you need to do is disable the native drag preview and you are good to go.

import { disableNativeDragPreview } from '@atlaskit/pragmatic-drag-and-drop/element/disable-native-drag-preview';

draggable({
    element: myElement,
    onGenerateDragPreview({ nativeSetDragPreview }) {
        disableNativeDragPreview({ nativeSetDragPreview });
    },
});

scrollJustEnoughIntoView

A little utility to quickly scroll something into view before a drag preview is captured. This is helpful if you are leveraging default drag previews (ie not using setCustomNativeDragPreview). If the draggable element is not completely in view, then the drag preview can be cut off.

import { scrollJustEnoughIntoView } from '@atlaskit/pragmatic-drag-and-drop/element/scroll-just-enough-into-view';

draggable({
    element: myElement,
    onGenerateDragPreview({ source }) {
        scrollJustEnoughIntoView({ element: source.element });
    },
});
Was this page helpful?
We use this feedback to improve our documentation.
© 2026 AtlassianTrademark, (opens new window)Privacy, (opens new window)License