<Draggable />
components can be dragged around and dropped onto <Droppable />
s. A <Draggable />
must always be contained within a <Droppable />
. It is possible to reorder a <Draggable />
within its home <Droppable />
or move to another <Droppable />
. It is possible because a <Droppable />
is free to control what it allows to be dropped on it.
Every <Draggable />
has a drag handle. A drag handle is the element that the user interacts with in order to drag a <Draggable />
. A drag handle can be the <Draggable />
element itself, or a child of the <Draggable />
. Note that by default a drag handle cannot be an interactive element, since event handlers are blocked on nested interactive elements. Proper semantics for accessibility are added to the drag handle element. If you wish to use an interactive element, disableInteractiveElementBlocking
must be set.
import { Draggable } from '@hello-pangea/dnd';
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<h4>My draggable</h4>
</div>
)}
</Draggable>;
interface Props {
// required
draggableId: DraggableId;
index: number;
children: ChildrenFn;
// optional
isDragDisabled?: boolean;
disableInteractiveElementBlocking?: boolean;
shouldRespectForcePress?: boolean;
}
@hello-pangea/dnd
will throw an error if a required prop is not provided
draggableId
: A requiredDraggableId(string)
. See our identifiers guide for more information.index
: A requirednumber
that matches the order of the<Draggable />
in the<Droppable />
. It is simply the index of the<Draggable />
in the list.
index
rule:
- Must be unique within a
<Droppable />
(no duplicates) - Must be consecutive.
[0, 1, 2]
and not[1, 2, 8]
Indexes do not need to start from 0
(this is often the case in virtual lists). In development mode we will log warnings to the console
if any of these rules are violated. See Setup problem detection and error recovery
Typically the index
value will simply be the index
provided by a Array.prototype.map
function:
{
this.props.items.map((item, index) => (
<Draggable draggableId={item.id} index={index}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.content}
</div>
)}
</Draggable>
));
}
isDragDisabled
: A flag to control whether or not the<Draggable />
is permitted to drag. You can use this to implement your own conditional drag logic. It will default tofalse
.disableInteractiveElementBlocking
: A flag to opt out of blocking a drag from interactive elements. For more information refer to the section Interactive child elements within a<Draggable />
shouldRespectForcePress
: Whether or not the drag handle should respect force press interactions. See Force press.
The React
children of a <Draggable />
must be a function that returns a ReactNode
.
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
Drag me!
</div>
)}
</Draggable>
type DraggableChildrenFn = (
DraggableProvided,
DraggableStateSnapshot,
DraggableRubric,
) => ReactNode | null;
The function is provided with three arguments:
interface DraggableProvided {
draggableProps: DraggableProps;
// will be null if the draggable is disabled
dragHandleProps: DragHandleProps | null;
innerRef: (a?: HTMLElement | null) => void;
}
For more type information please see our types guide.
Everything within the provided object must be applied for the <Draggable />
to function correctly.
provided.innerRef (innerRef: (HTMLElement) => void)
: In order for the<Draggable />
to function correctly, you must bind theinnerRef
function to theReactElement
that you want to be considered the<Draggable />
node. We do this in order to avoid needing to useReactDOM
to look up your DOM node.
For more information on using
innerRef
see our usinginnerRef
guide
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => <div ref={provided.innerRef}>Drag me!</div>}
</Draggable>
// Note: this will not work directly as we are not applying draggableProps or dragHandleProps
provided.draggableProps (DraggableProps)
: This is an Object that contains adata
attribute and an inlinestyle
. This Object needs to be applied to the same node that you applyprovided.innerRef
to. This controls the movement of the draggable when it is dragging and not dragging. You are welcome to add your own styles toDraggableProps.style
– but please do not remove or replace any of the properties.
// Props that can be spread onto the element directly
interface DraggableProps {
// inline style
style?: DraggableStyle;
// used for shared global styles
'data-rfd-draggable-context-id': ContextId;
'data-rfd-draggable-id': DraggableId; // used to know when a transition ends
onTransitionEnd?: TransitionEventHandler;
}
For more type information please see our types guide.
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
Drag me!
</div>
)}
</Draggable>
// Note: this will not work directly as we are not applying dragHandleProps
If you are rendering a list of <Draggable />
s then it is important that you add a key
prop to each <Draggable />
.
return items.map((item, index) => (
<Draggable
// adding a key is important!
key={item.id}
draggableId={item.id}
index={index}
>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.content}
</div>
)}
</Draggable>
));
Rules:
- Your
key
needs to be unique within the list - Your
key
should not include theindex
of the item
Usually you will want to just use the draggableId
as the key
React
will warn you if your list is missing keys
. It will not warn you if you are using index
as a part of your key
.
Not using keys
correctly will cause really bad times 💥
It is a contract of this library that it owns the positioning logic of the dragging element. This includes properties such as top
, right
, bottom
, left
and transform
. The library may change how it positions things and which properties it uses without performing a major version bump. It is also recommended that you do not apply your own transition
property to the dragging element.
@hello-pangea/dnd
uses position: fixed
to position the dragging element. This is quite robust and allows for you to have position: relative | absolute | fixed
parents. However, unfortunately position:fixed
is impacted by transform
(such as transform: rotate(10deg);
). This means that if you have a transform: *
on one of the parents of a <Draggable />
then the positioning logic will be incorrect while dragging. Lame! For most consumers this will not be an issue.
To get around this you can reparent your . We do not enable this functionality by default as it has performance problems.
Safari only
In Safari, it is possible for a user to perform a force press action. This is possible with a touch device (touchforcechange
) and with a mouse (webkitmouseforcechanged
).
We have found that in order to give the most consistent drag and drop experience we need to opt out of force press interactions on a drag handle. However, it is possible to have @hello-pangea/dnd
work while also respecting force press interactions. The trade off is that if we register a force press interaction a drag will be cancelled.
In order to control this behaviour you set the shouldRespectForcePress
prop on a <Draggable />
. By default we set this value to false
to prevent heavy presses from cancelling a drag.
If you set shouldRespectForcePress
to true
then the following will occur:
If the user force presses on the element before they have moved the element (even if a drag has already started) then the drag is cancelled and the standard force press action occurs. For an anchor this is a website preview.
Any force press action will cancel an existing or pending drag
See our focus guide
If you are using inline styles you are welcome to extend the DraggableProps.style
object. You are also welcome to apply the DraggableProps.style
object using inline styles and use your own styling solution for the component itself. You can use anything you like to style the <Draggable />
such as
- css-in-js such as
styled-components
oremotion
- sass, less
- vanilla css
If you are overriding inline styles be sure to do it after you spread the provided.draggableProps
or the spread will override your inline style.
<Draggable draggable="draggable-1" index={0}>
{(provided, snapshot) => {
// extending the DraggableStyle with our own inline styles
const style = {
backgroundColor: snapshot.isDragging ? 'blue' : 'white',
fontSize: 18,
...provided.draggableProps.style,
};
return (
<div ref={provided.innerRef} {...provided.draggableProps} style={style}>
Drag me!
</div>
);
}}
</Draggable>
Avoid margin collapsing between <Draggable />
s. margin collapsing is one of those really hard parts of CSS. For our purposes, if you have one <Draggable />
with a margin-bottom: 10px
and the next <Draggable />
has a margin-top: 12px
these margins will collapse and the resulting space between the elements will be the greater of the two: 12px
. When we do our calculations we are currently not accounting for margin collapsing. If you do want to have a margin on the siblings, wrap them both in a div
and apply the margin to the inner div
so they are not direct siblings.
It is an assumption that <Draggable />
s are visible siblings of one another. There can be other elements in between, but these elements should not take up any additional space. You probably will not do this anyway, but just calling it out to be super clear.
// Direct siblings ✅
<Draggable draggableId="draggable-1" index={0}>
{() => {}}
</Draggable>
<Draggable draggableId="draggable-2" index={1}>
{() => {}}
</Draggable>
// Not direct siblings, but are visible siblings ✅
<div>
<Draggable draggableId="draggable-1" index={0}>
{() => {}}
</Draggable>
</div>
<div>
<Draggable draggableId="draggable-2" index={1}>
{() => {}}
</Draggable>
</div>
// Spacer elements ❌
<Draggable draggableId="draggable-1" index={0}>
{() => {}}
</Draggable>
<p>I will break things!</p>
<Draggable draggableId="draggable-2" index={1}>
{() => {}}
</Draggable>
// Spacing on non sibling wrappers ❌
<div style={{padding: 10}}>
<Draggable draggableId="draggable-1" index={0}>
{() => {}}
</Draggable>
</div>
<div style={{padding: 10}}>
<Draggable draggableId="draggable-2" index={1}>
{() => {}}
</Draggable>
</div>
provided.dragHandleProps (?DragHandleProps)
every<Draggable />
has a drag handle. This is what is used to drag the whole<Draggable />
. Often this will be the same node as the<Draggable />
, but sometimes it can be a child of the<Draggable />
.DragHandleProps
need to be applied to the node that you want to be the drag handle. This is a number of props that need to be applied to the<Draggable />
node. The simplest approach is to spread the props onto the draggable node ({...provided.dragHandleProps}
). However, you are also welcome to monkey patch these props if you also need to respond to them. DragHandleProps will benull
whenisDragDisabled
is set totrue
.
interface DragHandleProps {
// what draggable the handle belongs to
'data-rfd-drag-handle-draggable-id': DraggableId;
// What DragDropContext the drag handle is in
'data-rfd-drag-handle-context-id': ContextId;
role: string;
// Id of hidden element that contains the lift instruction (nicer screen reader text)
'aria-labelledby': ElementId;
// Allow tabbing to this element
tabIndex: number;
// Stop html5 drag and drop
draggable: boolean;
onDragStart: DragEventHandler;
}
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
Drag me!
</div>
)}
</Draggable>
Controlling a whole draggable by just a part of it
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div ref={provided.innerRef} {...provided.draggableProps}>
<h2>Hello there</h2>
<div {...provided.dragHandleProps}>Drag handle</div>
</div>
)}
</Draggable>
interface DraggableStateSnapshot {
// Set to true if a Draggable is being actively dragged, or if it is drop animating
// Both active dragging and the drop animation are considered part of the drag
// *Generally this is the only property you will be using*
isDragging: boolean;
// Set to true if a Draggable is drop animating. Not every drag and drop interaction
// as a drop animation. There is no drop animation when a Draggable is already in its final
// position when dropped. This is commonly the case when dragging with a keyboard
isDropAnimating: boolean;
// Information about a drop animation
dropAnimation: DropAnimation | null;
// What Droppable (if any) the Draggable is currently over
draggingOver: DroppableId | null;
// the id of a draggable that you are combining with
combineWith: DraggableId | null;
// if something else is dragging and you are a combine target, then this is the id of the item that is dragging
combineTargetFor: DraggableId | null;
// There are two modes that a drag can be in
// 'FLUID': everything is done in response to highly granular input (eg mouse)
// 'SNAP': items snap between positions (eg keyboard);
mode: MovementMode | null;
}
See our type guide for more details
The children
function is also provided with a small amount of state relating to the current drag state. This can be optionally used to enhance your component. A common use case is changing the appearance of a <Draggable />
while it is being dragged. Note: if you want to change the cursor to something like grab
you will need to add the style to the draggable. (See Extending DraggableProps.style
above)
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => {
const style = {
backgroundColor: snapshot.isDragging ? 'blue' : 'grey',
...provided.draggableProps.style,
};
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={style}
>
Drag me!
</div>
);
}}
</Draggable>
interface DraggableRubric {
draggableId: DraggableId;
type: TypeId;
source: DraggableLocation;
}
rubric
represents all of the information associated with a <Draggable />
. rubric
is helpful for looking up the data associated with your <Draggable />
when it is not available in the current scope. This is useful when using the <Droppable /> | renderClone
API. The rubric
is the same lookup information that is provided to the Responder
s.
You are welcome to add your own onClick
handler to a <Draggable />
or a drag handle (which might be the same element). onClick
events handlers will always be called if a click occurred. If we are preventing the click, then the event.defaultPrevented
property will be set to true
. We prevent click events from occurring when the user was dragging an item. See sloppy clicks and click prevention for more information.
It is possible for your <Draggable />
to contain interactive elements. By default we block dragging on these elements. By doing this we allow those elements to function in the usual way. Here is the list of interactive elements that we block dragging from by default:
input
button
textarea
select
option
optgroup
video
audio
contenteditable
(any elements that arecontenteditable
or are within acontenteditable
container)
You can opt out of this behavior by adding the disableInteractiveElementBlocking
prop to a <Draggable />
. However, it is questionable as to whether you should be doing so because it will render the interactive element unusable. If you need to conditionally block dragging from interactive elements you can add the disableInteractiveElementBlocking
prop to opt out of the default blocking and monkey patch the dragHandleProps (DragHandleProps)
event handlers to disable dragging as required.