Beautiful, accessible drag and drop for lists with React.js
See how beautiful it is for yourself!
- Simple list
- Board - best viewed in landscape
We provide different links for touch devices as currently storybook does not have a good mobile menu experience more information
You can find an upgrade guide in our release notes.
- Beautiful, natural movement of items
- Clean and powerful api which is simple to get started with
- Plays extremely well with standard browser interactions
- Unopinionated styling
- No creation of additional wrapper dom nodes - flexbox and focus management friendly!
- Vertical lists ↕
- Horizontal lists ↔
- Movement between lists (▤ ↔ ▤)
- Mouse 🐭, keyboard 🎹 and touch 👉📱 (mobile, tablet and so on) support
- Conditional dragging and dropping
- Multiple independent lists on the one page
- Independent nested lists - a list can be a child of another list, but you cannot drag items from the parent list into a child list
- Flexible item sizes - the draggable items can have different heights (vertical) or widths (horizontal))
- Custom drag handle - you can drag a whole item by just a part of it
- A droppable list can be a scroll container (without a scrollable parent) or be the child of a scroll container (that also does not have a scrollable parent)
- Server side rendering compatible
- Plays well with nested interactive elements by default
You can check out all the features that will be landing soon on our issue page.
There are a lot of libraries out there that allow for drag and drop interactions within React. Most notable of these is the amazing react-dnd
. It does an incredible job at providing a great set of drag and drop primitives which work especially well with the wildly inconsistent html5 drag and drop feature. react-beautiful-dnd
is a higher level abstraction specifically built for vertical and horizontal lists. Within that subset of functionality react-beautiful-dnd
offers a powerful, natural and beautiful drag and drop experience. However, it does not provide the breadth of functionality offered by react-dnd. So this library might not be for you depending on what your use case is.
react-beautiful-dnd
uses position: fixed
to position the dragging element. In some layouts, this might break how the element is rendered. One example is a <table>
-based layout which will lose column widths for dragged <tr>
s. Follow #103 for updates on support for this use case.
We have created some basic examples for you to play with directly:
The core design idea of react-beautiful-dnd
is physicality: we want users to feel like they are moving physical objects around
It is a fairly standard drag and drop pattern for things to disappear and reappear in response to the users drag. For a more natural drag we animate the movement of items as they need to move out of the way while dragging to more clearly show a drags effect. We also animate the drop of an item so that it animates into its new home position. At no point is an item instantly moved anywhere — regardless of whether it is dragging or not.
It is quite common for drag and drop interactions to be based on the position that user started the drag from.
In react-beautiful-dnd
a dragging items impact is based on its centre of gravity — regardless of where a user grabs an item from. A dragging items impact follows similar rules to a set of scales ⚖️. Here are some rules that are followed to allow for a natural drag experience even with items of flexible height:
- A list is dragged over when the centre position of a dragging item goes over one of the boundaries of the list
- A resting drag item will move out of the way of a dragging item when the centre position of the dragging item goes over the edge of the resting item. Put another way: once the centre position of an item (A) goes over the edge of another item (B), B moves out of the way.
Drop shadows are useful in an environment where items and their destinations snap around. However, with react-beautiful-dnd
it should be obvious where things will be dropping based on the movement of items. This might be changed in the future - but the experiment is to see how far we can get without any of these affordances.
react-beautiful-dnd
works really hard to avoid as many periods of non-interactivity as possible. The user should feel like they are in control of the interface and not waiting for an animation to finish before they can continue to interact with the interface. However, there is a balance that needs to be made between correctness and power in order to make everybody's lives more sane. Here are the only situations where some things are not interactive:
- From when a user cancels a drag to when the drop animation completes. On cancel there are lots of things moving back to where they should be. If you grab an item in a location that is not its true home then the following drag will be incorrect.
- Starting a drag on an item that is animating its own drop. For simplicity this is the case - it is actually quite hard to grab something while it is animating home. It could be coded around - but it seems like an edge case that would add a lot of complexity.
Keep in mind that these periods of inactivity may not always exist.
For now, the library does not support drag axis locking (aka drag rails). This is where the user is restricted to only dragging along one axis. The current thinking is this breaks the physical metaphor we are going for and sends a message to the user that they are interacting with a piece of software rather than moving physical objects around. It is possible to ensure that a user can only drop in a single list by using props type
and isDropDisabled
. You can also do some visual treatment to the list onDragStart
to show the user that this is the only place they can interact with.
Rather than using an index based approach for keyboard movement between lists, react-beautiful-dnd
performs cross list movement based on inertia, gravity and collisions. You can find out more about how this works by reading the blog "Natural keyboard movement between lists".
With things moving a lot it would be easy for the user to become distracted by the animations or for them to get in the way. We have tweaked the various animations to ensure the right balance of guidance, performance and interactivity.
When you drop a dragging item its movement is based on physics (thanks react-motion
). This results in the drop feeling more weighted and physical.
Items that are moving out of the way of a dragging item do so with a CSS transition rather than physics. This is to maximise performance by allowing the GPU to handle the movement. The CSS animation curve has been designed to communicate getting out of the way.
How it is composed:
- A warm up period to mimic a natural response time
- A small phase to quickly move out of the way
- A long tail so that people can read any text that is being animated in the second half of the animation
animation curve used when moving out of the way
react-beautiful-dnd
does not create any wrapper elements. This means that it will not impact the usual tab flow of a document. For example, if you are wrapping an anchor tag then the user will tab to the anchor directly and not an element surrounding the anchor. Whatever element you wrap will be given a tab-index
to ensure that users can tab to the element to perform keyboard dragging.
Traditionally drag and drop interactions have been exclusively a mouse or touch interaction. This library ships with support for drag and drop interactions using only a keyboard. This enables power users to drive their experience entirely from the keyboard. As well as opening up these experiences to users who would have been excluded previously.
In addition to supporting keyboard, we have also audited how the keyboard shortcuts interact with standard browser keyboard interactions. When the user is not dragging they can use their keyboard as they normally would. While dragging we override and disable certain browser shortcuts (such as tab
) to ensure a fluid experience for the user.
When a user presses the mouse down on an element, we cannot determine if the user was clicking or dragging. Also, sometimes when a user clicks they can move the cursor slightly — a sloppy click. So we only start a drag once the user has moved beyond a certain distance with the mouse down (the drag threshold) — more than they would if they were just making a sloppy click. If the drag threshold is not exceeded then the user interaction behaves just like a regular click. If the drag threshold is exceeded then the interaction will be classified as a drag and the standard click action will not occur.
This allows consumers to wrap interactive elements such as an anchor and have it be both a standard anchor as well as a draggable item in a natural way.
(🐱🎁 is a schrodinger's cat joke)
When a drag is not occurring react-beautiful-dnd
does not impact any of the standard keyboard interactions (it has no listeners bound).
When a drag is occurring with a mouse the user is able to execute the following keyboard shortcuts:
- escape esc - cancel the drag
During a mouse drag the following standard keyboard events are blocked to prevent a bad experience:
- tab tab ↹ - blocking tabbing
- enter ⏎ - blocking submission
Other than these explicitly blocked keyboard events all standard keyboard events should work as expected.
react-beautiful-dnd
supports dragging with only a keyboard
When a drag is not occurring, key the user will be able to navigation through the Draggable
's on a page using the standard tab tab ↹ key to move forward through the tabbable elements and (shift + tab) (shift + )tab ↹) to move backwards. We achieve this by adding a tab-index
to the Draggable
. When a Draggable
has focus the spacebar space will lift a Draggable
. This will start the drag.
Once a drag is started the following keyboard shortcuts can be used:
- spacebar space - drop the
Draggable
- escape esc - cancel the drag
The following commands are also available but they depend on the type
of Droppable
that the Draggable
is currently in:
- Up arrow ↑ - move a
Draggable
upwards in aDroppable
- Down arrow ↓ - move a
Draggable
downwards in aDroppable
- Right arrow → - move a
Draggable
to aDroppable
to the right of the currentDroppable
(move to new list) - Left arrow ← - move a
Draggable
to aDroppable
to the left of the currentDroppable
(move to new list)
- Up arrow ↑ - move a
Draggable
to aDroppable
to above the currentDroppable
(move to new list) - Down arrow ↓ - move a
Draggable
to aDroppable
to below the currentDroppable
(move to new list) - Right arrow → - move a
Draggable
to the right in the currentDroppable
- Left arrow ← - move a
Draggable
to the left in the currentDroppable
During a drag the following standard keyboard events are blocked to prevent a bad experience:
- tab tab ↹ - blocking tabbing
- enter ⏎ - blocking submission
There is current limitation of keyboard dragging: the drag will cancel if the user scrolls the window. This could be worked around but for now it is the simplest initial approach.
react-beautiful-dnd
supports dragging on touch devices such as mobiles and tablets.
Recorded on iPhone 6s
When a user presses their finger (or other input) on a Draggable
we are not sure if they where intending to tap, force press, scroll the container or drag. As much as possible react-beautiful-dnd
aims to ensure that a users default interaction experience remains unaffected.
A user can start a drag by holding their finger 👇 on an element for a small period of time 🕑 (long press)
If the user lifts their finger before the timer is finished then we release the event to the browser for it to determine whether to perform the standard tap / click action. This allows you to have a Draggable
that is both clickable such as a anchor as well as draggable. If the item was dragged then we block the tap action from occurring.
If we detect a touchmove
before the long press timer expires we cancel the pending drag and allow the user to scroll normally. This means that the user needs to be fairly intentional and precise with their grabbing. Once the first touchmove
occurs we have to either opt in our out of native scrolling.
- If the long press timer has not expired: allow native scrolling and prevent dragging
- If the long press timer has expired: a drag has started and we prevent native scrolling
Safari only
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.
This is merely an idea - it is up to you to add this if you want this behavior.
If you like you could also trigger a vibration event when the user picks up a Draggable
. This can provide tactile feedback that the user is doing something. It currently is only supported in Chrome on Android.
class App extends React.Component {
onDragStart = () => {
// good times
if (window.navigator.vibrate) {
window.navigator.vibrate(100);
}
};
/*...*/
}
We apply a number of non-visible styles to facilitate the dragging experience. We do this using combination of styling targets and techniques. It is a goal of the library to provide unopinioned styling. However, we do apply some reasonable cursor
styling on drag handles by default. This is designed to make the library work as simply as possible out of the box. If you want to use your own cursors you are more than welcome to. All you need to do is override our cursor style rules by using a rule with higher specificity.
Here are the styles that are applied at various points in the drag lifecycle:
Styles applied to: drag handle element using the data-react-beautiful-dnd-drag-handle
attribute.
A long press on anchors usually pops a content menu that has options for the link such as 'Open in new tab'. Because long press is used to start a drag we need to opt out of this behavior
-webkit-touch-callout: none;
Webkit based browsers add a grey overlay to anchors when they are active. We remove this tap overlay as it is confusing for users. more information.
-webkit-tap-highlight-color: rgba(0,0,0,0);
Avoid the pull to refresh action and delayed anchor focus on Android Chrome
touch-action: manipulation;
Styles applied to: drag handle element using the data-react-beautiful-dnd-drag-handle
attribute.
Adding a cursor style to let the user know this element is draggable. You are welcome to override this.
cursor: grab;
Styles applied using the data-react-beautiful-dnd-drag-handle
attribute
An optimisation to avoid processing pointer-events
while dragging. Also used to allow scrolling through a drag handle with a track pad or mouse wheel.
point-events: none;
Styles applied using the data-react-beautiful-dnd-draggable
attribute
This is what we use to control Draggable
s that need to move out of the way of a dragging Draggable
.
transition: ${string};
Styles applied using inline styles
This is described by the type DraggableStyle
.
We apply a cursor while dragging to give user feedback that a drag is occurring. You are welcome to override this. A good point to do this is the onDragStart
event.
cursor: grabbing;
To prevent the user selecting text as they drag apply this style
user-select: none;
Styles applied using the data-react-beautiful-dnd-drag-handle
attribute
We apply the grab cursor to all drag handles except the drag handle for the dropping Draggable
. At this point the user is able to drag other Draggable
's if they like.
cursor: grab;
Same as dragging phase
When a user explicitly cancels a drag
This is the same as Phase: dropping
. However we do not apply a cursor: grab
to the drag handle. During a user initiated cancel we do not allow the dragging of other items until the drop animation is complete.
All styles applied are vendor prefixed correctly to meet the requirements of our supported browser matrix. This is done by hand to avoid adding to react-beautiful-dnd's size by including a css-in-js library
# yarn
yarn add react-beautiful-dnd
# npm
npm install react-beautiful-dnd --save
A universal module definition bundle is published on npm
under the /dist
folder for consumption . We publish the following files:
dist/react-beautiful-dnd.js
dist/react-beautiful-dnd.min.js
(minified bundle)
These bundles list react
as an external which needs to be provided. This is done to reduce the size of the bundle and prevent consumers from loading react
multiple times. You can provide react
through your module system or simply by having react
on the window
.
You can use the UMD to run react-beautiful-dnd
directly in the browser.
<!-- peer dependency -->
<script src="https://unpkg.com/[email protected]/dist/react.js"></script>
<!-- lib (change x.x.x for the version you would like) -->
<script src="https://unpkg.com/[email protected]/dist/react-beautiful-dnd.js"></script>
<!-- needed to mount your react app -->
<script src="https://unpkg.com/[email protected]/dist/react-dom.js"></script>
<script>
const React = window.React;
const ReactDOM = window.ReactDOM;
const { DragDropContext, Draggable, Droppable } = window.ReactBeautifulDnd;
class App extends React.Component {
//...
}
// You can use JSX if your environment supports it
ReactDOM.render(React.createElement(App), document.getElementById('app'));
</script>
There is also an example codepen you can use to play with this installation method.
You can consume react-beautiful-dnd
from within ClojureScript
using CLJSJS!
Okay, into the fun stuff - so how do you use the library?
In order to use drag and drop, you need to have the part of your React
tree that you want to be able to use drag and drop in wrapped in a DragDropContext
. It is advised to just wrap your entire application in a DragDropContext
. Having nested DragDropContext
's is not supported. You will be able to achieve your desired conditional dragging and dropping using the props of Droppable
and Draggable
. You can think of DragDropContext
as having a similar purpose to the react-redux Provider component
type Hooks = {|
onDragStart?: (initial: DragStart) => void,
onDragEnd: (result: DropResult) => void,
|}
type Props = {|
...Hooks,
children?: ReactElement,
|}
import { DragDropContext } from 'react-beautiful-dnd';
class App extends React.Component {
onDragStart = () => {
/*...*/
};
onDragEnd = () => {
/*...*/
};
render() {
return (
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<div>Hello world</div>
</DragDropContext>
);
}
}
These are top level application events that you can use to perform your own state updates.
This function will get notified when a drag starts. You are provided with the following details:
initial.draggableId
: the id of theDraggable
that is now dragginginitial.type
: thetype
of theDraggable
that is now dragginginitial.source
: the location (droppableId
andindex
) of where the dragging item has started within aDroppable
.
This function is optional and therefore does not need to be provided. It is highly recommended that you use this function to block updates to all Draggable
and Droppable
components during a drag. (See *Best practices for hooks
*)
onDragStart?: (initial: DragStart) => void
// supporting types
type DragStart = {|
draggableId: DraggableId,
type: TypeId,
source: DraggableLocation,
|}
type DraggableLocation = {|
droppableId: DroppableId,
// the position of the draggable within a droppable
index: number
|};
type Id = string;
type DraggableId = Id;
type DroppableId = Id;
type TypeId = Id;
This function is extremely important and has an critical role to play in the application lifecycle. This function must result in the synchronous reordering of a list of Draggables
It is provided with all the information about a drag:
result.draggableId
: the id of theDraggable
that was dragging.result.type
: thetype
of theDraggable
that was dragging.result.source
: the location where theDraggable
started.result.destination
: the location where theDraggable
finished. Thedestination
will benull
if the user dropped into no position (such as outside any list) or if they dropped theDraggable
back into the same position in which it started.
Because this library does not control your state, it is up to you to synchronously reorder your lists based on the result
.
- if the
destination
isnull
: all done! - if
source.droppableId
equalsdestination.droppableId
you need to remove the item from your list and insert it at the correct position. - if
source.droppableId
does not equaldestination.droppableId
, then you need to remove theDraggable
from thesource.droppableId
list and add it into the correct position of thedestination.droppableId
list.
If you need to persist a reorder to a remote data store - update the list synchronously on the client and fire off a request in the background to persist the change. If the remote save fails it is up to you how to communicate that to the user and update, or not update, the list.
onDragEnd: (result: DropResult) => void
// supporting types
type DropResult = {|
draggableId: DraggableId,
type: TypeId,
source: DraggableLocation,
// may not have any destination (drag to nowhere)
destination: ?DraggableLocation
|}
type Id = string;
type DroppableId = Id;
type DraggableId = Id;
type TypeId = Id;
type DraggableLocation = {|
droppableId: DroppableId,
// the position of the droppable within a droppable
index: number
|};
It is highly recommended that while a user is dragging that you block any state updates that might impact the amount of Draggable
s and Droppable
s, or their dimensions. Please listen to onDragStart
and block updates to the Draggable
s and Droppable
s until you receive at onDragEnd
.
When the user starts dragging we take a snapshot of all of the dimensions of the applicable Draggable
and Droppable
nodes. If these change during a drag we will not know about it.
Here are a few poor user experiences that can occur if you change things during a drag:
- If you increase the amount of nodes, then the library will not know about them and they will not be moved when the user would expect them to be.
- If you decrease the amount of nodes, then there might be gaps and unexpected movements in your lists.
- If you change the dimensions of any node, then it can cause the changed node as well as others to move at incorrect times.
- If you remove the node that the user is dragging, then the drag will instantly end
- If you change the dimension of the dragging node, then other things will not move out of the way at the correct time.
When an item is moved from one list to a different list, it loses browser focus if it had it. This is because React
creates a new node in this situation. It will not lose focus if transitioned within the same list. The dragging item will always have had browser focus if it is dragging with a keyboard. It is highly recommended that you give the item (which is now in a different list) focus again. You can see an example of how to do this in our stories. Here is an example of how you could do it:
onDragEnd
: move the item into the new list and record the id of the item that has moved- When rendering the reordered list, pass down a prop which will tell the newly moved item to obtain focus
- In the
componentDidMount
lifecycle call back check if the item needs to gain focus based on its props (such as anautoFocus
prop) - If focus is required - call
.focus
on the node. You can obtain the node by usingReactDOM.findDOMNode
or monkey patching theprovided.innerRef
callback.
We try very hard to ensure that each onDragStart
event is paired with a single onDragEnd
event. However, there maybe a rogue situation where this is not the case. If that occurs - it is a bug. Currently there is no mechanism to tell the library to cancel a current drag externally.
Droppable
components can be dropped on by a Draggable
. They also contain Draggable
s. A Draggable
must be contained within a Droppable
.
import { Droppable } from 'react-beautiful-dnd';
<Droppable droppableId="droppable-1" type="PERSON">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
>
<h2>I am a droppable!</h2>
{provided.placeholder}
</div>
)}
</Droppable>;
droppableId
: A requiredDroppableId(string)
that uniquely identifies the droppable for the application. Please do not change this prop - especially during a drag.type
: An optionalTypeId(string)
that can be used to simply accept a class ofDraggable
. For example, if you use the typePERSON
then it will only allowDraggable
s of typePERSON
to be dropped on itself.Draggable
s of typeTASK
would not be able to be dropped on aDroppable
with typePERSON
. If notype
is provided, it will be set to'DEFAULT'
. Currently thetype
of theDraggable
s within aDroppable
must be the same. This restriction might be loosened in the future if there is a valid use case.isDropDisabled
: An optional flag to control whether or not dropping is currently allowed on theDroppable
. You can use this to implement your own conditional dropping logic. It will default tofalse
.direction
: The direction in which items flow in this droppable. Options arevertical
(default) andhorizontal
.ignoreContainerClipping
: When aDroppable
is inside a scrollable container its area is constrained so that you can only drop on the part of theDroppable
that you can see. Setting this prop opts out of this behavior, allowing you to drop anywhere on aDroppable
even if it's visually hidden by a scrollable parent. The default behavior is suitable for most cases so odds are you'll never need to use this prop, but it can be useful if you've got very longDraggable
s inside a short scroll container. Keep in mind that it might cause some unexpected behavior if you have multipleDroppable
s inside scroll containers on the same page.
The React
children of a Droppable
must be a function that returns a ReactElement
.
<Droppable droppableId="droppable-1">
{(provided, snapshot) => ({
/*...*/
})}
</Droppable>;
The function is provided with two arguments:
type DroppableProvided = {|
innerRef: (?HTMLElement) => void,
placeholder: ?ReactElement,
|}
-
provided.innerRef
: In order for the droppable to function correctly, you must bind theprovided.innerRef
to the highest possible DOM node in theReactElement
. We do this in order to avoid needing to useReactDOM
to look up your DOM node. -
provided.placeholder
: This is used to create space in theDroppable
as needed during a drag. This space is needed when a user is dragging over a list that is not the home list. Please be sure to put the placeholder inside of the component for which you have provided the ref. We need to increase the side of theDroppable
itself. This is different fromDraggable
where theplaceholder
needs to be a sibling to the draggable node.
<Droppable droppableId="droppable-1">
{(provided, snapshot) => (
<div ref={provided.innerRef}>
Good to go
{provided.placeholder}
</div>
)}
</Droppable>;
type DroppableStateSnapshot = {|
isDraggingOver: boolean,
|};
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 Droppable
while it is being dragged over.
<Droppable droppableId="droppable-1">
{(provided, snapshot) => (
<div
ref={provided.innerRef}
style={{ backgroundColor: snapshot.isDraggingOver ? 'blue' : 'grey' }}
>
I am a droppable!
{provided.placeholder}
</div>
)}
</Droppable>;
Droppable
s can only be dropped on byDraggable
s who share the sametype
. This is a simple way of allowing conditional dropping. If you do not provide atype
for theDroppable
, then it will only acceptDraggable
s which also have the default type.Draggable
s andDroppable
s both will have theirtypes
set to'DEFAULT'
when none is provided. There is currently no way to set multipletypes
, or atype
wildcard that will acceptDraggable
s of multiple any types. This could be added if there is a valid use case.- Using the
isDropDisabled
prop you can conditionally allow dropping. This allows you to do arbitrarily complex conditional transitions. This will only be considered if thetype
of theDroppable
matches thetype
of the currently draggingDraggable
. - You can disable dropping on a
Droppable
altogether by always settingisDropDisabled
to false. You can do this to create a list that is never able to be dropped on, but containsDraggable
s. - Technically you do not need to use
type
and do all of your conditional drop logic with theisDropDisabled
function. Thetype
parameter is a convenient shortcut for a common use case.
This library supports dragging within scroll containers (DOM elements that have overflow: auto;
or overflow: scroll;
). The only supported use cases are:
- The
Droppable
can itself be a scroll container with no scrollable parents - The
Droppable
has one scrollable parent
It is recommended that you put a min-height
on a vertical Droppable
or a min-width
on a horizontal Droppable
. Otherwise when the Droppable
is empty there may not be enough of a target for Draggable
being dragged with touch or mouse inputs to be over the Droppable
.
When a user drags over, or stops dragging over, a Droppable
we re-render the Droppable
with an updated DroppableStateSnapshot > isDraggingOver
value. This is useful for styling the Droppable
. However, by default this will cause a render of all of the children of the Droppable
- which might be 100's of Draggable
s! This can result in a noticeable frame rate drop. To avoid this problem we recommend that you create a component that is the child of a Droppable
who's responsibility it is to avoid rendering children if it is not required.
Here is an example of how you could do this:
import React, { Component } from 'react';
class Student extends Component<{ student: Person }> {
render() {
// Renders out a draggable student
}
}
class InnerList extends Component<{ students: Person[] }> {
// do not re-render if the students list has not changed
shouldComponentUpdate(nextProps: Props) {
if(this.props.students === nextProps.students) {
return false;
}
return true;
}
// You could also not do your own shouldComponentUpdate check and just
// extend from React.PureComponent
render() {
return this.props.students.map((student: Person) => (
<Student student={student} />
))
}
}
class Students extends Component {
render() {
return (
<Droppable droppableId="list">
{(provided: DroppableProvided, snapshot: DroppableStateSnapshot) => (
<div
ref={provided.innerRef}
style={{ backgroundColor: provided.isDragging ? 'green' : 'lightblue' }}
>
<InnerList students={this.props.students} />
{provided.placeholder}
</div>
)}
</Droppable>
)
}
}
By using the approach you are able to make style changes to a Droppable
when it is being dragged over, but you avoid re-rendering all of the children unnecessarily. Keep in mind that if you are using React.PureComponent
that your component will not respond to changes in the context.
Unfortunately we are unable to apply this optimisation for you. It is a byproduct of using the function-as-child pattern.
Currently auto scrolling of scroll containers is not part of this library. Auto scrolling is where the container automatically scrolls to make room for the dragging item as you drag near the edge of a scroll container. You are welcome to build your own auto scrolling list, or if you would you really like it as part of this library we could provide a auto scrolling Droppable
.
Users will be able to scroll a scroll container while dragging by using their trackpad or mouse wheel.
Getting keyboard dragging to work with scroll containers is quite difficult. Currently there is a limitation: you cannot drag with a keyboard beyond the visible edge of a scroll container. This limitation could be removed if we introduced auto scrolling. Scrolling a container with a mouse during a keyboard drag will cancel the drag.
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.
import { Draggable } from 'react-beautiful-dnd';
<Draggable draggableId="draggable-1" type="PERSON" index={0}>
{(provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<h4>My draggable</h4>
</div>
{provided.placeholder}
</div>
)}
</Draggable>;
Note: when the library moves to React 16 this will be cleaned up a little bit as we will be able to return the placeholder as a sibling to your child function without you needing to create a wrapping element
draggableId
: A requiredDraggableId(string)
that uniquely identifies theDraggable
for the application. Please do not change this prop - especially during a drag.type
: An optional type (TypeId(string)
) of theDraggable
. This is used to control whatDroppable
s theDraggable
is permitted to drop on.Draggable
s can only drop onDroppable
s that share the sametype
. If notype
is provided, then it will be set to'DEFAULT'
. Currently thetype
of aDraggable
must be the same as its containerDroppable
. This restriction might be loosened in the future if there is a valid use case.isDragDisabled
: An optional flag to control whether or not theDraggable
is permitted to drag. You can use this to implement your own conditional drag logic. It will default tofalse
.disableInteractiveElementBlocking
: An optional flag to opt out of blocking a drag from interactive elements. For more information refer to the section Interactive child elements within aDraggable
The React
children of a Draggable
must be a function that returns a ReactElement
.
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
Drag me!
</div>
{provided.placeholder}
</div>
)}
</Draggable>;
The function is provided with two arguments:
type DraggableProvided = {|
innerRef: (HTMLElement) => void,
draggableProps: ?DraggableProps,
dragHandleProps: ?DragHandleProps,
placeholder: ?ReactElement,
|}
Everything within the provided object must be applied for the Draggable
to function correctly.
provided.innerRef (innerRef: (HTMLElement) => void)
: In order for theDroppable
to function correctly, you must bind theinnerRef
function to theReactElement
that you want to be considered theDraggable
node. We do this in order to avoid needing to useReactDOM
to look up your DOM node.
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => <div ref={provided.innerRef}>Drag me!</div>}
</Draggable>;
innerRef: (HTMLElement) => void
provided.draggableProps (?DraggableProps)
: This is an Object that contains adata
attribute and an inline style. 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
export type DraggableProps = {|
// inline style
style: ?DraggableStyle,
// used for shared global styles
'data-react-beautiful-dnd-draggable': string,
|}
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div>
<div ref={provided.innerRef} {...provided.draggableProps}>
Drag me!
</div>
</div>
)}
</Draggable>;
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.
react-beautiful-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.
This will be changing soon as we move to a portal solution where we will be appending the Draggable
to the end of the body to avoid any parent transforms. If you really need this feature right now we have created an example where we implement a portal on top of the current api. Please note however, this technique is not officially supported and might break in minor / patch releases.
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 - such as styled-components.
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>
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={style}
>
Drag me!
</div>
</div>
);
}}
</Draggable>;
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 margin 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 and 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 visual 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>
type DraggingStyle = {|
pointerEvents: 'none',
position: 'fixed',
width: number,
height: number,
pointerEvents: 'none',
boxSizing: 'border-box',
top: number,
left: number,
margin: 0,
transition: 'none',
transform: ?string,
zIndex: ZIndex,
|}
type NotDraggingStyle = {|
transition: ?string,
transition: null | 'none',
|};
provided.placeholder (?ReactElement)
TheDraggable
element hasposition: fixed
applied to it while it is dragging. The role of theplaceholder
is to sit in the place that theDraggable
was during a drag. It is needed to stop theDroppable
list from collapsing when you drag. It is advised to render it as a sibling to theDraggable
node. This is unlikeDroppable
where theplaceholder
needs to be within theDroppable
node. When the library moves toReact
16 theplaceholder
will be removed from api.
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div>
<div ref={provided.innerRef} {...provided.draggableProps}>
Drag me!
</div>
{/* Always render me - I will be null if not required */}
{provided.placeholder}
</div>
)}
</Draggable>;
provided.dragHandleProps (?DragHandleProps)
everyDraggable
has a drag handle. This is what is used to drag the wholeDraggable
. Often this will be the same node as theDraggable
, but sometimes it can be a child of theDraggable
.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 theDraggable
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
.
type DragHandleProps = {|
onMouseDown: (event: MouseEvent) => void,
onKeyDown: (event: KeyboardEvent) => void,
onClick: (event: MouseEvent) => void,
tabIndex: number,
'aria-grabbed': boolean,
draggable: boolean,
onDragStart: () => void,
onDrop: () => void,
|};
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
Drag me!
</div>
{provided.placeholder}
</div>
)}
</Draggable>;
Controlling a whole draggable by just a part of it
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => (
<div>
<div ref={provided.innerRef} {...provided.draggableProps}>
<h2>Hello there</h2>
<div {...provided.dragHandleProps}>Drag handle</div>
</div>
{provided.placeholder}
</div>
)}
</Draggable>;
You can override some of the dragHandleProps
props with your own behavior if you need to.
const myOnClick = event => console.log('clicked on', event.target);
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => {
const onClick = (() => {
// dragHandleProps might be null
if (!provided.dragHandleProps) {
return myOnClick;
}
// creating a new onClick function that calls my onClick
// event as well as the provided one.
return event => {
provided.dragHandleProps.onClick(event);
// You may want to check if event.defaultPrevented
// is true and optionally fire your handler
myOnClick(event);
};
})();
return (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
onClick={onClick}
>
Drag me!
</div>
{provided.placeholder}
</div>
);
}}
</Draggable>;
type DraggableStateSnapshot = {|
isDragging: boolean,
|};
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 body. (See DragDropContext
> style above)
<Draggable draggableId="draggable-1" index={0}>
{(provided, snapshot) => {
const style = {
backgroundColor: snapshot.isDragging ? 'blue' : 'grey',
...provided.draggableProps.style,
};
return (
<div>
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
style={style}
>
Drag me!
</div>
{provided.placeholder}
</div>
);
}}
</Draggable>;
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 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.
react-beautiful-dnd
is typed using flowtype
. This greatly improves internal consistency within the codebase. We also expose a number of public types which will allow you to type your javascript if you would like to. If you are not using flowtype
this will not inhibit you from using the library. It is just extra safety for those who want it.
// id's
type Id = string;
type TypeId = Id;
type DroppableId = Id;
type DraggableId = Id;
// hooks
type DropResult = {|
draggableId: DraggableId,
type: TypeId,
source: DraggableLocation,
// may not have any destination (drag to nowhere)
destination: ?DraggableLocation
|}
type DraggableLocation = {|
droppableId: DroppableId,
// the position of the droppable within a droppable
index: number
|};
// Droppable
type DroppableProvided = {|
innerRef: (?HTMLElement) => void,
placeholder: ?ReactElement,
|}
type DroppableStateSnapshot = {|
isDraggingOver: boolean,
|}
// Draggable
type DraggableProvided = {|
innerRef: (?HTMLElement) => void,
draggableProps: ?DraggableProps,
dragHandleProps: ?DragHandleProps,
placeholder: ?ReactElement,
|}
type DraggableStateSnapshot = {|
isDragging: boolean,
|}
export type DraggableProps = {|
style: ?DraggableStyle,
'data-react-beautiful-dnd-draggable': string,
|}
type DraggableStyle = DraggingStyle | NotDraggingStyle
type DraggingStyle = {|
pointerEvents: 'none',
position: 'fixed',
width: number,
height: number,
boxSizing: 'border-box',
pointerEvents: 'none',
top: number,
left: number,
margin: 0,
transition: 'none',
transform: ?string,
zIndex: ZIndex,
|}
type NotDraggingStyle = {|
transition: ?string,
transition: null | 'none',
|}
type DragHandleProvided = {|
onMouseDown: (event: MouseEvent) => void,
onKeyDown: (event: KeyboardEvent) => void,
onClick: (event: MouseEvent) => void,
onTouchStart: (event: TouchEvent) => void,
onTouchMove: (event: TouchEvent) => void,
tabIndex: number,
'aria-grabbed': boolean,
draggable: boolean,
onDragStart: () => boolean,
onDrop: () => boolean
|}
The types are exported as part of the module so using them is as simple as:
import type { DroppableProvided } from 'react-beautiful-dnd';
If you are using TypeScript you can use the community maintained DefinitelyTyped type definitions. Installation instructions.
We have created a sample application which exercises the flowtypes. It is a super simple React
project based on react-create-app
. You can use this as a reference to see how to set things up correctly.
This codebase is typed with flowtype to promote greater internal consistency and more resilient code.
This code base employs a number of different testing strategies including unit, performance and integration tests. Testing various aspects of the system helps to promote its quality and stability.
While code coverage is not a guarantee of code health, it is a good indicator. This code base currently sits at ~90% coverage.
This codebase is designed to be extremely performant - it is part of its DNA. It builds on prior investigations into React
performance that you can read about here and here. It is designed to perform the minimum number of renders required for each task.
- using connected-components with memoization to ensure the only components that render are the ones that need to - thanks
react-redux
,reselect
andmemoize-one
- all movements are throttled with a
requestAnimationFrame
- thanksraf-schd
- memoization is used all over the place - thanks
memoize-one
- conditionally disabling
pointer-events
onDraggable
s while dragging to prevent the browser needing to do redundant work - you can read more about the technique here - Non primary animations are done on the GPU
Great care has been taken to keep the library as light as possible. It is currently ~34kb (gzip) in size. There could be a smaller net cost if you where already using one of the underlying dependencies.
This library supports the standard Atlassian supported browsers for desktop:
Desktop | Version |
---|---|
Microsoft Internet Explorer(Windows) | Version 11 |
Microsoft Edge | Latest stable version supported |
Mozilla Firefox (all platforms) | Latest stable version supported |
Google Chrome (Windows and Mac) | Latest stable version supported |
Safari (Mac) | Latest stable version on latest OS release supported |
Mobile | Version |
---|---|
Chrome (Android and iOS) | Latest stable version supported |
Mobile Safari (iOS) | Latest stable version supported |
Android (Android) | The default browser on Android 4.0.3 (Ice Cream Sandwich) |
The documentation for this library is also available in other languages:
- Korean: leehyunggeun/react-beautiful-dnd
These translations are maintained by the community and are not reviewed or maintained by the maintainers of this library. Please raise issues on the respective translations if you would like to have them updated.
Alex Reardon - @alexandereardon - [email protected]
Jared Crowe - @jaredjcrowe - [email protected]