Drag and Drop
Drag and drop primitives help you build pointer-driven reorder UIs with a grip beside your row content.
Atlas integrates with stacks such as react-grid-layout: the handle exposes the .draggableHandle class so dragging starts only from the grip, not from the rest of the row.
Reordering uses pointer dragging inside react-grid-layout (not the HTML draggable attribute). To lock a row at the atlas grip, set disabled on DragAndDrop.Item. Setting draggable={false} on inner markup does not affect reordering here.
Example
Sortable rows with DragAndDrop
Reorder rows inside DragAndDrop, keep order in React state with onLayoutChange, and wrap each row surface in DragAndDrop.Item (inside a keyed wrapper so grid items stay stable).
For cols === 1 with compactType='vertical', Atlas merges grip-disabled static then runs compact on y beside pinned rows (utils.compact) — sort keyed children by y from onLayoutChange (below) beside disabled rows for sane stacking and height. Multi-column stacks or compactType={null} skip that step automatically.
function DragAndDropReorderDemo() { const [items, setItems] = useState([ { id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta' }, { id: 'c', label: 'Gamma' }, ]); const handleLayoutChange = (newLayout) => { const yFor = (id) => newLayout.find((l) => String(l.i) === String(id))?.y ?? 0; const sorted = [...items].sort((a, b) => { const dy = yFor(a.id) - yFor(b.id); if (dy !== 0) return dy; /** Stable tie-break when y coords collide momentarily mid-drag */ return String(a.id).localeCompare(String(b.id)); }); if (sorted.some((item, i) => item.id !== items[i].id)) { setItems(sorted); } }; return ( <div className="combo-box-rgl-live"> <div className="combo-box-rgl-demo" style={{ maxWidth: 520 }}> <p style={{ marginTop: 0 }}> Drag via the <strong>grip icon only </strong>(not the gray row)—the combo / field ignores drags elsewhere. </p> <DragAndDrop width={520} onLayoutChange={handleLayoutChange}> {items.map((item, index) => ( <div key={item.id}> <DragAndDrop.Item> <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '8px 12px', background: '#f3f4f6', borderRadius: 6, border: '1px solid #e5e7eb', }} > <span style={{ color: '#6b7280', minWidth: 28 }}> {index + 1}. </span> <span style={{ flex: 1, fontWeight: 500 }}>{item.label}</span> </div> </DragAndDrop.Item> </div> ))} </DragAndDrop> <p style={{ fontSize: 13, color: '#4b5563', marginBottom: 0 }}> Current order: {items.map((i) => i.label).join(' → ')} </p> </div> </div> ); }
Combo Box beside DragAndDrop.Item
When the atlas grip should sit outside the bordered react-select shell, wrap ComboBox in DragAndDrop.Item. Use one keyed wrapper per grid row (<div key={…}>).
While your sortable host is moving a row, set dragging on that item from your layout lifecycle (Atlas only styles chrome). Use disabled when that row must not reorder—the combo stays editable; only the grip becomes an inert spacer.
This live block puts the disabled combo first so it stays anchored at y=0; the rows below share one grip each—you can reorder those two freely. Putting disabled last instead behaves the same for the draggable pair, but dragging the pinned slot itself (attempting “third row → second” when row 3 is locked) remains impossible by design.
Reorder row keys from onLayoutChange (same y sort as the simple demo) when a disabled row wraps a slot—Atlas applies compact for vertical gaps when compactType='vertical'.
function DragAndDropComboBoxDemo() { const options = [ { value: 'apple', label: 'Apple' }, { value: 'mango', label: 'Mango' }, ]; const comboShared = { blockScroll: false, options, }; /** Pinned grip-disabled row `row-pinned` is first (`static`) — rows below reorder by grip. Keys sort from `onLayoutChange`. */ const rowDefs = [ { key: 'row-pinned', dragging: false, disabled: true, 'aria-label': 'Pinned top — reorder locked here; combo still usable elsewhere', placeholder: 'Grip disabled — row stays fixed', }, { key: 'row-default', dragging: false, disabled: false, 'aria-label': 'Reorder via grip — swap with row below', placeholder: 'Movable combo A', }, { key: 'row-second', dragging: false, disabled: false, 'aria-label': 'Reorder via grip — swap with row above', placeholder: 'Movable combo B', }, ]; const [rowKeys, setRowKeys] = useState([ 'row-pinned', 'row-default', 'row-second', ]); const handleLayoutChange = (newLayout) => { const yFor = (id) => newLayout.find((l) => String(l.i) === String(id))?.y ?? 0; const next = [...rowKeys].sort((a, b) => { const dy = yFor(a) - yFor(b); if (dy !== 0) return dy; return String(a).localeCompare(String(b)); }); if (next.some((key, idx) => key !== rowKeys[idx])) { setRowKeys(next); } }; const defsByKey = new Map(rowDefs.map((cfg) => [cfg.key, cfg])); return ( <div className="combo-box-rgl-live"> <div className="combo-box-rgl-demo" style={{ maxWidth: 520 }}> <p style={{ marginTop: 0, fontSize: 13 }}> Swap <strong>Movable A</strong> and <strong>Movable B</strong> by dragging{' '} <strong>only the grips</strong> (not the fields). The{' '} <strong>pinned</strong> top row cannot be reordered — Atlas marks it{' '} <code>static</code>, so it keeps the top slot (<code>y=0</code>) while the two movable combos swap underneath. </p> <DragAndDrop width={520} rowHeight={64} margin={[0, 10]} onLayoutChange={handleLayoutChange} > {rowKeys.map((key) => { const cfg = defsByKey.get(key); if (!cfg) { return null; } return ( <div key={cfg.key}> <DragAndDrop.Item dragging={cfg.dragging} disabled={cfg.disabled}> <ComboBox {...comboShared} aria-label={cfg['aria-label']} placeholder={cfg.placeholder} /> </DragAndDrop.Item> </div> ); })} </DragAndDrop> </div> </div> ); }
Multi-column Tile grid with DragAndDrop.Item
Same Tile + react-grid-layout layout as above, but each cell wraps the tile in DragAndDrop.Item so the atlas grip sits beside the tile. Turn off draggable on Tile here—otherwise both the tile grip and the item grip would match .draggableHandle. Keep resizable on Tile so resize still uses the tile chrome. Pass a fixed width, keep layout in state, and set compactType to null for dashboard-style placement.
function DragAndDropTileGridWithItemDemo() { const [layout, setLayout] = useState([ { i: 'a', x: 0, y: 0, w: 1, h: 2, minH: 2 }, { i: 'b', x: 1, y: 0, w: 2, h: 2, minH: 2 }, { i: 'c', x: 3, y: 0, w: 1, h: 2, minH: 2 }, ]); return ( <div className="combo-box-rgl-live"> <div className="combo-box-rgl-demo" style={{ maxWidth: 520 }}> <p style={{ marginTop: 0 }}> Drag the atlas grip beside each tile; resize from the tile handle. Layout updates via `onLayoutChange`. </p> <DragAndDrop width={520} layout={layout} onLayoutChange={setLayout} cols={4} rowHeight={30} compactType={null} isResizable margin={[8, 8]} > <div key="a"> <DragAndDrop.Item> <Tile resizable>A</Tile> </DragAndDrop.Item> </div> <div key="b"> <DragAndDrop.Item> <Tile resizable>B</Tile> </DragAndDrop.Item> </div> <div key="c"> <DragAndDrop.Item> <Tile resizable>C</Tile> </DragAndDrop.Item> </div> </DragAndDrop> </div> </div> ); }
Usage Rules
When to use
- You want a single-column stack where only the atlas grip moves the row—compose with Combo Box and
DragAndDroporDragAndDrop.Itembeside fields. - You import
DragAndDropandDragAndDrop.Itemfrom@adjust/components. StandaloneDragHandle,DragAndDropLayout, and DragHandleItem-style splits are not public exports. - Your host expects a draggable-handle selector (
draggableHandle=".draggableHandle"is fixed on the Atlas wrapper). - You want stable hooks for tests or CSS (
atlas-drag-handle-layout,atlas-drag-handle-item,atlas-sortable-drag-handle).
When not to use
- Rows do not reorder by themselves—you still own item order (
onLayoutChangeor another sortable host). These primitives handle layout and visuals only. - The grip has no explanatory visible label by default; add surrounding context if needed (Atlas sets
aria-label="Drag to reorder"on the active grip).
Dependencies, CSS, and width
Add react-grid-layout alongside @adjust/components when you use the grid root. react-grid-layout itself depends on react-draggable (and react-resizable, resize-observer-polyfill); you do not need to install react-draggable directly—installing react-grid-layout brings it in.
Importing DragAndDrop from @adjust/components pulls in react-grid-layout/css/styles.css. If you mount raw ReactGridLayout alone, import that stylesheet yourself.
Provide width in pixels—often via ResizeObserver or layout measurement—because the wrapper follows react-grid-layout’s contract.
For fields with the grip outside the bordered control, wrap ComboBox in DragAndDrop.Item (Combo Box reorder example). For drag-and-resize tiles, see Tile. Toggle dragging on DragAndDrop.Item from your host while a drag is active.
Controlled layout
Each immediate child under DragAndDrop should use a stable React key, typically <div key={id}>…</div>. onLayoutChange reports y positions keyed by grid item id (i). Omitting layout lets Atlas derive placements from keyed children while onLayoutChange still runs; pass layout for a fully controlled grid (Combo Box reorder example).
Stacks that include disabled grips should reorder keyed children after onLayoutChange so DOM order tracks sorted y.
Setting disabled on DragAndDrop.Item removes .draggableHandle, inserts an inert spacer, and marks that grid item static internally so compaction and sibling drags cannot move that slot (first/middle/last row). preventCollision is forwarded from your props (react-grid-layout); when undefined, Atlas passes false so movable rows can swap next to static slots. Pass preventCollision={true} only if you want RGL’s stricter overlap blocking (it can prevent adjacent swaps). For cols === 1 with compactType='vertical', Atlas re-applies static pins then compact so stray y lanes don’t inflate grid height (multi-column / compactType={null} skips that). allowOverlap is forwarded untouched. When isResizable is enabled on DragAndDrop, locked rows keep isResizable: true on their layout slice so Tiles can still resize from their own chrome.
Props
DragAndDrop
| Name | Type | Default |
|---|---|---|
style Inline-style object to pass to the root element. |
| — |
className The classname to add to the root element.
Class on the outer wrapper with atlas-drag-handle-layout. |
| — |
cols Number of columns in this layout. |
| — |
margin Margin between items [x, y] in px. |
| — |
containerPadding Padding inside the container [x, y] in px. |
| — |
layout Layout is an array of object with the format:
{x: number, y: number, w: number, h: number}
The index into the layout must match the key used on each item component.
If you choose to use custom keys, you can specify that key in the layout
array objects like so:
`{i: string, x: number, y: number, w: number, h: number}`
If not provided, use data-grid props on children. |
| — |
onLayoutChange Callback so you can save the layout.
Calls back with (currentLayout) after every drag or resize stop. |
| — |
width This allows setting the initial width on the server side.
This is required unless using the HOC <WidthProvider> or similar. |
| — |
autoSize If true, the container height swells and contracts to fit contents. |
| — |
draggableCancel A CSS selector for tags that will not be draggable.
For example: draggableCancel: '.MyNonDraggableAreaClassName'
If you forget the leading. it will not work.
"".react-resizable-handle" is always prepended to this value. |
| — |
compactType Compaction type. |
| — |
rowHeight Rows have a static height, but you can change this based on breakpoints if you like. |
| — |
droppingItem Configuration of a dropping element. Dropping element is a "virtual" element
which appears when you drag over some element from outside. |
| — |
verticalCompact If true, the layout will compact vertically. |
| — |
maxRows Default Infinity, but you can specify a max here if you like.
Note that this isn't fully fleshed out and won't error if you specify a layout that
extends beyond the row capacity. It will, however, not allow users to drag/resize
an item past the barrier. They can push items beyond the barrier, though.
Intentionally not documented for this reason. |
| — |
isDraggable If set to false it will disable dragging on all children. |
| — |
isResizable If set to false it will disable resizing on all children. |
| — |
isBounded If true and draggable, all items will be moved only within grid. |
| — |
useCSSTransforms Uses CSS3 translate() instead of position top/left.
This makes about 6x faster paint performance. |
| — |
transformScale If parent DOM node of ResponsiveReactGridLayout or ReactGridLayout has "transform: scale(n)" css property,
we should set scale coefficient to avoid render artefacts while dragging. |
| — |
allowOverlap If true, grid can be placed one over the other. |
| — |
preventCollision If true, grid items won't change position when being dragged over. |
| — |
isDroppable If true, droppable elements (with draggable={true} attribute)
can be dropped on the grid. It triggers "onDrop" callback
with position and event object as parameters.
It can be useful for dropping an element in a specific position
NOTE: In case of using Firefox you should add
`onDragStart={e => e.dataTransfer.setData('text/plain', '')}` attribute
along with Link |
| — |
resizeHandles Defines which resize handles should be rendered
Allows for any combination of:
's' - South handle (bottom-center)
'w' - West handle (left-center)
'e' - East handle (right-center)
'n' - North handle (top-center)
'sw' - Southwest handle (bottom-left)
'nw' - Northwest handle (top-left)
'se' - Southeast handle (bottom-right)
'ne' - Northeast handle (top-right) |
| — |
resizeHandle Defines custom component for resize handle |
| — |
onDragStart Calls when drag starts. |
| — |
onDrag Calls on each drag movement. |
| — |
onDragStop Calls when drag is complete. |
| — |
onResizeStart Calls when resize starts. |
| — |
onResize Calls when resize movement happens. |
| — |
onResizeStop Calls when resize is complete. |
| — |
onDrop Calls when some element has been dropped |
| — |
onDropDragOver Calls when an element is being dragged over the grid from outside as above.
This callback should return an object to dynamically change the droppingItem size
Return false to short-circuit the dragover |
| — |
innerRef Ref for getting a reference for the grid's wrapping div.
You can use this instead of a regular ref and the deprecated ReactDOM.findDOMNode()` function. |
| — |
gridClassName Extra class names on the ReactGridLayout root. |
| — |
data-{foo} Data attributes can be used by testing libraries to retrieve components or assert their existence |
| — |
| * - the prop is required. |
DragAndDrop.Item
DragAndDrop.Item lays out children beside the atlas grip.
| Name | Type | Default |
|---|---|---|
disabled When true, the interactive handle is omitted (inert spacer). Reordering uses pointer
dragging inside ** react-grid-layout**, not the HTML **`draggable`** attribute—inner
DOM **`draggable={false}`** does not lock the row; use **`disabled`** here instead. |
| false |
dragging Styles the row/handle as in an active drag. |
| false |
className |
| — |
contentClassName |
| — |
dragHandleDataTestId Overrides default data-testid on the interactive handle. |
| — |
aria-label ** aria-label** on the draggable grip (**`Drag to reorder`** when omitted).
Use localized copy for non-default locales. |
| — |
data-{foo} Data attributes can be used by testing libraries to retrieve components or assert their existence |
| — |
| * - the prop is required. |
Accessibility
When DragAndDrop.Item is not disabled, the grip is a button with default aria-label="Drag to reorder". When disabled, the spacer is aria-hidden so keyboard users skip it. react-grid-layout is pointer-heavy; add keyboard reordering in your application if you need full parity.