Historical input note: This file is retained as the original seed spec for the repository. It is not the living product contract. For current truth, use
docs/product/*,docs/stories/*, anddocs/decisions/*. For the Jaspr initiative, seeSPEC_JASPR.mdonly as historical design input.
dnd_kit is a drag-and-drop toolkit for Flutter, inspired by React dnd-kit but redesigned around Flutter’s widget, render, gesture, and state models.
The main goal is to provide a generic draggable/droppable engine powerful enough to build:
- basic drag and drop;
- sortable lists;
- sortable grids;
- Kanban boards;
- dashboard builders;
- canvas editors;
- form/page builders;
- admin UIs for web and desktop;
- mobile UIs with long-press dragging.
dnd_kit v1 treats mobile, web, and desktop as first-class targets from the beginning.
dnd_kit must satisfy the following goals:
-
Generic engine first, presets second The core must solve the draggable/droppable problem first. Sortable and Kanban are preset/showcase layers, not the foundation of the architecture.
-
Flutter-style API Public API should use Flutter-style naming such as
DndScope,DndDraggable,DndDroppable,DndDragOverlay,SortableScope, andSortableItem. -
Production-grade The library must be predictable, testable, extensible, and suitable for real apps, not just demos.
-
Composable Users should be able to integrate it with
setState, Riverpod, BLoC, Provider, Redux, or any other state management approach without being locked in. -
Extensible The library must support custom sensors, collision detectors, modifiers, measuring strategies, drag overlays, and sortable strategies.
-
Stable public API early Core public APIs should be designed and frozen early to avoid painful breaking changes later.
-
Type-safe with Dart 3 The implementation should take advantage of Dart 3 features such as
sealed class,interface class,final class, pattern matching, and exhaustive switch statements.
V1 is not intended to replace native OS-level drag and drop.
The following are not part of stable v1:
- dragging files from Finder/Explorer into a Flutter app;
- dragging items from a Flutter app to the OS;
- native file drag and drop;
- cross-window drag and drop;
- cross-app drag and drop;
- full virtualized variable-height sortable lists;
- complex nested sortable layouts;
- highly opinionated animation systems;
- dependency on external state management libraries such as Riverpod, Provider, or BLoC.
Native OS drag and drop, if supported later, should be implemented in a separate package:
dnd_kit_native
The project should use a mono-repo structure.
dnd_kit/
packages/
dnd_kit_core/
dnd_kit_flutter/
dnd_kit_sortable/
dnd_kit/
examples/
basic_drag_drop/
drag_overlay/
sortable_list/
sortable_grid/
kanban_board/
custom_collision/
custom_sensor/
auto_scroll/
keyboard_accessibility/
docs/
architecture.md
api_design.md
collision_detection.md
sensors.md
modifiers.md
measuring.md
sortable.md
accessibility.md
Pure Dart package.
No Flutter dependency.
- DndId
- DndPoint
- DndSize
- DndRect
- DndTransform
- DndState
- DndDragSession
- DndActive
- DndOver
- DndCollision
- DndCollisionDetector
- built-in collision algorithms
- DndModifier
- sensor contracts
- event models
- registry contracts
- base sortable math
- Widget
- BuildContext
- RenderBox
- Flutter Offset / Rect / Size
- AnimationController
- GestureDetector
- OverlayEntry
Flutter adapter package.
Depends on:
dependencies:
dnd_kit_core: ^0.1.0
flutter:
sdk: flutter- DndScope
- DndController
- DndDraggable
- DndDroppable
- DndDragHandle
- DndDragOverlay
- pointer/mouse/touch/keyboard sensors
- measuring system
- Flutter geometry adapter
- auto-scroll
- overlay rendering
- accessibility/semantics
Sortable preset package.
Depends on:
dependencies:
dnd_kit_core: ^0.1.0
dnd_kit_flutter: ^0.1.0- SortableScope
- SortableItem
- SortableContainer
- SortableStrategy
- SortableStrategies
- SortableMoveDetails
- sortable keyboard coordinates
Stable in v1:
- vertical list
- horizontal list
- grid
Experimental in v1:
- multi-container sortable
- nested sortable
- virtualized adapter
Umbrella package.
Exports all public APIs from the sub-packages:
library dnd_kit;
export 'package:dnd_kit_core/dnd_kit_core.dart';
export 'package:dnd_kit_flutter/dnd_kit_flutter.dart';
export 'package:dnd_kit_sortable/dnd_kit_sortable.dart';Users can install the full package:
dependencies:
dnd_kit: ^0.1.0Or use only the core package:
dependencies:
dnd_kit_core: ^0.1.0Recommended:
environment:
sdk: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"Reasons:
- modern enough to use Dart 3 type-system features;
- not too new, so adoption remains broad;
- suitable for a production package with a type-safe API;
- enables clean modeling of state machines, events, constraints, and extension points.
Keep runtime dependencies minimal.
dependencies:
collection: ^1.19.0
meta: ^1.15.0Use collection for:
- equality helpers
- iterable utilities
- sorting helpers
- small data-structure utilities
Use meta for:
- @immutable
- @internal
- @visibleForTesting
- @experimental
- @mustCallSuper
- vector_math
- provider
- riverpod
- bloc
- freezed
- equatable
Core geometry should define its own DndPoint, DndRect, and DndTransform to keep the API clean and stable.
dev_dependencies:
test: any
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
fake_async: ^1.3.0fake_async is useful for deterministic tests involving timers, delays, long press behavior, measuring throttling, and auto-scroll.
Use:
DndScope
DndDraggable
DndDroppable
DndDragHandle
DndDragOverlay
SortableScope
SortableItem
Avoid React-style naming:
DndContext
useDraggable
useDroppable
DragOverlayContext
Reason: Flutter developers are already familiar with concepts such as Scope, Controller, Builder, Item, and Handle.
DndScope must support both controlled and uncontrolled modes.
DndScope(
child: App(),
)DndScope creates and owns an internal controller.
final controller = DndController();
DndScope(
controller: controller,
child: App(),
)Rules:
- If the user provides a controller, the user owns its lifecycle.
- If no controller is provided, DndScope creates and disposes an internal controller.
- DndScope must not dispose an external controller.
The library must not mutate user data.
Sortable example:
SortableScope(
ids: tasks.map((e) => DndId(e.id)).toList(),
onReorder: (details) {
setState(() {
final item = tasks.removeAt(details.oldIndex);
tasks.insert(details.newIndex, item);
});
},
child: ...
)The library reports intent. The application owns mutation.
DndId must be stable during the widget lifecycle.
final class DndId {
const DndId(this.value);
final Object value;
@override
bool operator ==(Object other) {
return other is DndId && other.value == value;
}
@override
int get hashCode => value.hashCode;
}Avoid:
DndId(UniqueKey())
DndId(DateTime.now())
DndId(Object())Prefer:
DndId(task.id)
DndId('column-todo')
DndId(user.id)@immutable
final class DndPoint {
const DndPoint(this.x, this.y);
final double x;
final double y;
}
@immutable
final class DndSize {
const DndSize(this.width, this.height);
final double width;
final double height;
}
@immutable
final class DndRect {
const DndRect({
required this.left,
required this.top,
required this.width,
required this.height,
});
final double left;
final double top;
final double width;
final double height;
double get right => left + width;
double get bottom => top + height;
DndPoint get center => DndPoint(
left + width / 2,
top + height / 2,
);
}@immutable
final class DndTransform {
const DndTransform({
this.x = 0,
this.y = 0,
this.scaleX = 1,
this.scaleY = 1,
});
final double x;
final double y;
final double scaleX;
final double scaleY;
static const zero = DndTransform();
}sealed class DndState {
const DndState();
}
final class DndIdle extends DndState {
const DndIdle();
}
final class DndPending extends DndState {
const DndPending({
required this.activeId,
required this.origin,
});
final DndId activeId;
final DndPoint origin;
}
final class DndDragging extends DndState {
const DndDragging({
required this.session,
});
final DndDragSession session;
}
final class DndDropping extends DndState {
const DndDropping({
required this.session,
});
final DndDragSession session;
}
final class DndCancelled extends DndState {
const DndCancelled({
required this.reason,
});
final DndCancelReason reason;
}@immutable
final class DndDragSession {
const DndDragSession({
required this.active,
required this.initialPointer,
required this.currentPointer,
required this.delta,
required this.collisions,
required this.over,
required this.inputKind,
});
final DndActive active;
final DndPoint initialPointer;
final DndPoint currentPointer;
final DndPoint delta;
final List<DndCollision> collisions;
final DndOver? over;
final DndInputKind inputKind;
}class DndScope extends StatefulWidget {
const DndScope({
super.key,
this.controller,
this.sensors = const [
DndPointerSensor(),
DndKeyboardSensor(),
],
this.collisionDetector = DndCollisionDetectors.closestCenter,
this.modifiers = const [],
this.measuring = const DndMeasuringConfig(),
this.autoScroll = const DndAutoScrollConfig.disabled(),
this.onDragStart,
this.onDragMove,
this.onDragOver,
this.onDragEnd,
this.onDragCancel,
required this.child,
});
final DndController? controller;
final List<DndSensor> sensors;
final DndCollisionDetector collisionDetector;
final List<DndModifier> modifiers;
final DndMeasuringConfig measuring;
final DndAutoScrollConfig autoScroll;
final DndDragStartCallback? onDragStart;
final DndDragMoveCallback? onDragMove;
final DndDragOverCallback? onDragOver;
final DndDragEndCallback? onDragEnd;
final DndDragCancelCallback? onDragCancel;
final Widget child;
}class DndDraggable<T> extends StatefulWidget {
const DndDraggable({
super.key,
required this.id,
this.data,
this.disabled = false,
this.activationConstraint,
this.feedback = const DndFeedback.child(),
this.builder,
required this.child,
});
final DndId id;
final T? data;
final bool disabled;
final DndActivationConstraint? activationConstraint;
final DndFeedback feedback;
final DndDraggableWidgetBuilder? builder;
final Widget child;
}DndDraggable<Task>(
id: DndId(task.id),
data: task,
child: TaskCard(
leading: const DndDragHandle(
child: Icon(Icons.drag_indicator),
),
title: task.title,
),
)Rules:
- If DndDraggable has no DndDragHandle child, the whole child acts as the drag activator.
- If one or more DndDragHandle widgets exist, only handles can activate dragging.
class DndDroppable<T> extends StatefulWidget {
const DndDroppable({
super.key,
required this.id,
this.data,
this.disabled = false,
this.accepts,
this.builder,
required this.child,
});
final DndId id;
final T? data;
final bool disabled;
final DndAcceptsCallback? accepts;
final DndDroppableWidgetBuilder? builder;
final Widget child;
}Example:
DndDroppable<BoardColumn>(
id: DndId(column.id),
data: column,
accepts: (active, self) => active.data is Task,
builder: (context, state, child) {
return DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
width: state.isOver ? 2 : 1,
),
),
child: child,
);
},
child: ColumnView(column),
)DndDragOverlay(
builder: (context, active) {
final task = active.data as Task;
return TaskCard(
task: task,
elevated: true,
);
},
)Rules:
- Overlay renders independently from the source layout.
- Overlay does not affect the size/layout of the original widget.
- Overlay follows the pointer using transform/listenable updates.
- Overlay must not rebuild the entire app on every pointer move.
typedef DndCollisionDetector = List<DndCollision> Function(
DndCollisionArgs args,
);final class DndCollisionArgs {
const DndCollisionArgs({
required this.activeRect,
required this.droppables,
required this.pointer,
});
final DndRect activeRect;
final List<DndDroppableEntry> droppables;
final DndPoint? pointer;
}abstract final class DndCollisionDetectors {
static List<DndCollision> closestCenter(DndCollisionArgs args) {}
static List<DndCollision> closestCorners(DndCollisionArgs args) {}
static List<DndCollision> rectIntersection(DndCollisionArgs args) {}
static List<DndCollision> pointerWithin(DndCollisionArgs args) {}
static DndCollisionDetector compose(
List<DndCollisionDetector> detectors,
) {}
}final kanbanCollisionDetector = DndCollisionDetectors.compose([
DndCollisionDetectors.pointerWithin,
DndCollisionDetectors.closestCenter,
]);abstract interface class DndModifier {
const DndModifier();
DndTransform apply(DndModifierArgs args);
}final class DndModifierArgs {
const DndModifierArgs({
required this.transform,
required this.activeRect,
required this.pointer,
this.containerRect,
});
final DndTransform transform;
final DndRect activeRect;
final DndPoint pointer;
final DndRect? containerRect;
}RestrictToVerticalAxis
RestrictToHorizontalAxis
RestrictToWindowBounds
RestrictToParentBounds
RestrictToScrollableAncestor
SnapToGrid
ClampOffset
DndPointerSensor
DndMouseSensor
DndTouchSensor
DndLongPressSensor
DndKeyboardSensor
sealed class DndActivationConstraint {
const DndActivationConstraint();
}
final class DndDistanceConstraint extends DndActivationConstraint {
const DndDistanceConstraint(this.distance);
final double distance;
}
final class DndDelayConstraint extends DndActivationConstraint {
const DndDelayConstraint({
required this.delay,
required this.tolerance,
});
final Duration delay;
final double tolerance;
}
final class DndLongPressConstraint extends DndActivationConstraint {
const DndLongPressConstraint({
required this.delay,
this.hapticFeedback = true,
});
final Duration delay;
final bool hapticFeedback;
}The measuring system must:
- measure draggable rects;
- measure droppable rects;
- cache rects by DndId;
- convert local/global coordinates consistently;
- re-measure when scroll or layout changes;
- avoid calling findRenderObject on every pointer move;
- avoid running collision detection more than once per frame.
enum DndMeasuringStrategy {
beforeDragging,
whileDragging,
always,
}final class DndMeasuringConfig {
const DndMeasuringConfig({
this.strategy = DndMeasuringStrategy.whileDragging,
this.throttle,
this.measureDroppablesOnScroll = true,
this.measureActiveDraggable = true,
});
final DndMeasuringStrategy strategy;
final Duration? throttle;
final bool measureDroppablesOnScroll;
final bool measureActiveDraggable;
}final class DndAutoScrollConfig {
const DndAutoScrollConfig({
required this.enabled,
this.threshold = 48,
this.maxSpeed = 24,
this.axis,
});
const DndAutoScrollConfig.enabled()
: enabled = true,
threshold = 48,
maxSpeed = 24,
axis = null;
const DndAutoScrollConfig.disabled()
: enabled = false,
threshold = 48,
maxSpeed = 24,
axis = null;
final bool enabled;
final double threshold;
final double maxSpeed;
final Axis? axis;
}- scroll the nearest scrollable container to the pointer;
- support vertical ListView;
- support horizontal scrolling;
- support basic nested scrolling;
- stop immediately on drop/cancel;
- do not scroll when the pointer is not near an edge.
SortableScope(
ids: tasks.map((task) => DndId(task.id)).toList(),
strategy: SortableStrategies.verticalList,
onReorder: (details) {
setState(() {
final task = tasks.removeAt(details.oldIndex);
tasks.insert(details.newIndex, task);
});
},
child: ListView(
children: [
for (final task in tasks)
SortableItem(
id: DndId(task.id),
child: TaskCard(task),
),
],
),
)SortableItem(
id: DndId(task.id),
child: TaskCard(task),
)abstract final class SortableStrategies {
static const verticalList = VerticalListSortableStrategy();
static const horizontalList = HorizontalListSortableStrategy();
static const grid = GridSortableStrategy();
}V1 should include model types for future multi-container support, but the full API does not need to be stable yet.
@experimental
final class SortableContainer {
const SortableContainer({
required this.id,
required this.itemIds,
});
final DndId id;
final List<DndId> itemIds;
}final class SortableMoveDetails {
const SortableMoveDetails({
required this.activeId,
required this.fromContainerId,
required this.toContainerId,
required this.oldIndex,
required this.newIndex,
});
final DndId activeId;
final DndId fromContainerId;
final DndId toContainerId;
final int oldIndex;
final int newIndex;
bool get isSameContainer => fromContainerId == toContainerId;
}Set up the mono-repo, package structure, coding conventions, API direction, and basic CI.
As a maintainer, I want the repo to be split into clear packages so core, Flutter adapter, sortable preset, and umbrella package can be developed independently.
- Repo has the following structure:
packages/dnd_kit_core
packages/dnd_kit_flutter
packages/dnd_kit_sortable
packages/dnd_kit
examples/
docs/
- Each package has its own
pubspec.yaml. dnd_kitexports all three sub-packages.melos bootstrapruns successfully.dart analyzepasses for the whole repo.- Root
README.mdexplains the library goal. docs/architecture.mdexists.
As a Flutter developer, I want to install only dnd_kit and get access to core, Flutter, and sortable APIs.
- This import works:
import 'package:dnd_kit/dnd_kit.dart';- After importing, the user can access:
DndId
DndScope
DndDraggable
DndDroppable
DndDragOverlay
SortableScope
SortableItem
- Example app compiles with only
dnd_kitas a dependency.
As an advanced developer, I want to install only dnd_kit_core so I can use collision, modifier, and state-machine logic without Flutter.
dnd_kit_coredoes not import Flutter.dart testruns in the core package without Flutter SDK.dnd_kit_coreexposes geometry, collision, and state-machine APIs.- Core
pubspec.yamlhas no Flutter dependency.
Build the pure Dart foundation: ID, geometry, state machine, event models, collision detectors, and modifier contracts.
As an app developer, I want to identify draggable and droppable elements with stable IDs so the library can track items correctly across rebuilds.
DndIdexists.DndIdacceptsObject value.- Two
DndIds with the same value are equal. DndIdcan be used as a key inMap<DndId, ...>.- Equality/hashCode tests exist.
- Duplicate IDs in the same registry are detected in debug mode.
As a maintainer, I want the core package to have its own geometry types so collision algorithms can be tested independently from Flutter.
DndPoint,DndSize,DndRect, andDndTransformexist.- Core does not import
dart:ui. - Core does not import Flutter.
- Helper methods exist:
DndRect.center
DndRect.right
DndRect.bottom
DndRect.contains
DndRect.intersects
- Unit tests exist for geometry methods.
As a maintainer, I want the drag lifecycle to be modeled by a clear state machine to avoid inconsistent runtime state.
- These states exist:
DndIdle
DndPending
DndDragging
DndDropping
DndCancelled
- Valid transitions:
idle -> pending
pending -> dragging
pending -> cancelled
dragging -> dropping
dragging -> cancelled
dropping -> idle
cancelled -> idle
- Invalid transitions are rejected in debug mode.
- Unit tests cover valid and invalid transitions.
- State is modeled using
sealed class.
As an app developer, I want to choose or write a collision detector to decide which target the dragged item is over.
DndCollisionDetectortypedef exists.- Built-in detectors exist:
closestCenter
closestCorners
rectIntersection
pointerWithin
compose
- Collision detectors receive only geometry data, not
BuildContext. - Unit tests cover multiple rect scenarios.
composereturns the first detector result that has non-empty collisions.- Collision results are sorted by clear score/ranking.
As an app developer, I want to restrict drag movement by axis, bounds, or grid.
DndModifierexists.- Built-in modifiers exist:
RestrictToVerticalAxis
RestrictToHorizontalAxis
SnapToGrid
- Modifiers can be composed in order.
- Unit tests exist for each modifier.
- Modifiers do not depend on Flutter.
Create DndScope, DndController, DndDraggable, DndDroppable, registry, basic measuring, and drag event lifecycle.
As a Flutter developer, I want to wrap widgets with DndDraggable and DndDroppable so I can receive drag/drop events.
- This example works:
DndScope(
onDragEnd: (event) {},
child: Column(
children: [
DndDraggable(
id: DndId('item'),
child: Text('Drag me'),
),
DndDroppable(
id: DndId('target'),
child: Text('Drop here'),
),
],
),
)onDragStartis called exactly once when drag starts.onDragMoveis called when the pointer moves.onDragOveris called when the active item is over a droppable.onDragEndis called on pointer up.onDragCancelis called on cancel.- Widget test simulates a successful drag.
As a Flutter developer, I want to use DndScope without manually creating a controller.
DndScope(child: ...)works.- Internal controller is created in
initState. - Internal controller is disposed in
dispose. - No memory leak in basic widget test.
- Public API does not require
controller.
As an advanced developer, I want to pass a DndController so I can inspect or control drag state.
DndScope(controller: controller, child: ...)works.- External controller is not disposed by
DndScope. - User can listen to controller state.
- Controller exposes at minimum:
state
active
over
cancel()
- Calling
controller.cancel()during dragging triggers the cancel lifecycle.
As a maintainer, I want draggable and droppable entries to register/unregister safely according to widget lifecycle.
- Draggable registers when mounted.
- Droppable registers when mounted.
- Entry unregisters on dispose.
- Entry updates when widget id/data/disabled changes.
- Duplicate ID asserts in debug mode.
- Release mode does not crash unnecessarily because of duplicate ID; warning is sent through diagnostics.
Support pointer, mouse, touch, long press, and keyboard input for mobile, web, and desktop.
As a web/desktop developer, I want to drag items using mouse/pointer input.
- Mouse primary button can start dragging.
- Right click does not start dragging by default.
- Pointer move updates drag position.
- Pointer up ends drag.
- Pointer cancel triggers cancel.
- Works on Flutter web and desktop examples.
As an app developer, I want dragging to start only after the pointer has moved a minimum distance to prevent accidental drags.
DndDistanceConstraintexists.- If movement is smaller than the distance, state remains pending.
- If movement is greater than or equal to the distance, state becomes dragging.
- Unit/widget tests cover threshold behavior.
- Default desktop/web threshold should be around 4–8 px.
As a mobile developer, I want dragging to start only after long press to avoid conflicts with scroll/tap.
DndLongPressSensororDndLongPressConstraintexists.- Long press after required delay starts dragging.
- Pointer movement beyond tolerance before delay cancels pending drag.
- Pointer up before delay does not start dragging.
- Widget tests use fake async.
hapticFeedbackoption exists.
As an app developer, I want only a small part of an item to activate dragging.
DndDragHandlewidget exists.- If
DndDraggablecontainsDndDragHandle, drag only starts from the handle. - If no handle exists, the whole child is the activator.
- Multiple handles in one draggable are supported.
- Drag handle works with mouse, touch, and keyboard focus.
- Dedicated example exists.
As a keyboard user, I want to pick up, move, drop, and cancel items using the keyboard.
- Space/Enter can pick up focused item.
- Arrow keys move the active item.
- Escape cancels.
- Space/Enter drops.
- Semantics hint exists.
- Widget tests cover keyboard flow.
- Does not crash when no droppable exists.
Ensure layout measuring is accurate, collision detection is stable, and drag performance remains acceptable.
As an app developer, I want collision detection to use the current layout accurately.
- On drag start, the library measures the active draggable and droppables.
- Rects are converted into the same coordinate space.
- Disabled droppables do not participate in collision.
- Unmounted droppables do not participate in collision.
- Widget tests cover multiple droppables.
As a maintainer, I want to avoid measuring layout on every pointer move.
- Pointer move does not measure all droppables every time.
- Measuring cache is invalidated on scroll or layout changes.
- Collision runs on cached rects.
- Debug counter exists in test/dev mode to track measurement count.
- Collision pass runs at most once per frame.
As an advanced developer, I want to pass a custom collision detector for special use cases like Kanban or canvas editors.
DndScope(collisionDetector: customDetector)works.- Custom detector receives
activeRect,pointer, anddroppables. - Custom detector may return an empty list.
- If empty list is returned,
event.over == null. - Custom collision example exists.
As an app developer, I want to restrict dragging by axis or grid.
DndScope(modifiers: [...])works.- Modifiers are applied in order.
- Built-in vertical/horizontal restriction works.
- Snap-to-grid works.
- Dedicated modifier example exists.
- Core unit tests and Flutter widget tests exist.
Complete the production drag/drop experience with overlay, visual state, and auto-scroll.
As an app developer, I want to render a separate overlay widget while dragging.
DndDragOverlayexists.- Overlay follows the pointer.
- Overlay does not occupy layout space.
- Overlay does not intercept pointer by default.
- Overlay disappears after drop/cancel.
- Overlay builder receives active item data.
- Drag overlay example exists.
As an app developer, I want to change the UI of an item while it is being dragged.
DndDraggable.builderreceives state.- State has:
isDragging
isPressed
isDisabled
transform
- Source item can be rendered with opacity 0.4 while dragging.
- The whole
DndScopeis not rebuilt on every pointer move. - Widget tests verify state changes.
As an app developer, I want to highlight a droppable when an item is hovering over it.
DndDroppable.builderreceives state.- State has:
isOver
isActive
canAccept
collision
- Droppable has
isOver == trueonly when it is the current candidate. acceptscallback affectscanAccept.- Highlight drop zone example exists.
As an app developer, I want scrollable containers to auto-scroll when dragging near their edges.
DndAutoScrollConfig.enabled()works.- Vertical
ListViewauto-scrolls near top/bottom. - Horizontal scroll auto-scrolls near left/right.
- Auto-scroll stops on drop/cancel.
- Auto-scroll does not run when the pointer is not near an edge.
- Auto-scroll example exists.
- Minimal vertical auto-scroll widget/integration test exists.
Provide production-usable vertical list, horizontal list, and grid sorting.
As an app developer, I want to create a sortable vertical list with a simple API.
SortableScope+SortableItemworks with vertical list.onReorderreturnsoldIndex,newIndex, andactiveId.- User mutates the list manually.
- Non-active items animate to projected positions.
- Dragging items up/down works reliably.
sortable_listexample exists.- Widget test reorders index 0 to index 2.
As an app developer, I want to reorder items in a horizontal list.
horizontalListstrategy works.- Horizontal-axis collision is accurate.
onReorderreturns correct indexes.- Example exists.
- Widget test exists.
As an app developer, I want to reorder items in a grid.
gridstrategy works withGridView.- Dragging items across rows/columns returns correct indexes.
- Other items animate to projected positions.
sortable_gridexample exists.- Widget test drags index 0 to index 5.
As an app developer, I want sortable items to be draggable only from a handle.
SortableItemis compatible withDndDragHandle.- Dragging outside the handle does not start when a handle exists.
- Dragging from the handle starts normally.
- Example exists.
As a keyboard user, I want to reorder items using the keyboard.
- Focus item.
- Space/Enter picks up.
- Arrow key changes projected index.
- Space/Enter drops.
- Escape cancels.
onReorderis called correctly on drop.- Semantics hint exists.
- Widget test covers keyboard reorder.
Use Kanban as a realistic showcase proving that the generic engine is powerful enough.
As a developer evaluating the library, I want to see a realistic Kanban board demo to understand whether the library is powerful enough.
kanban_board example includes:
- multiple columns;
- multiple task/cards;
- dragging cards within the same column;
- dragging cards to another column;
- drag overlay;
- droppable highlight;
- vertical auto-scroll inside columns;
- horizontal auto-scroll for board;
- custom collision detector;
- source item opacity while dragging.
- Demo runs on web.
- Demo runs on desktop.
- Demo runs on mobile at a basic level.
As an app developer, I want to build Kanban without being forced to use SortableScope.multi.
- First Kanban demo uses generic APIs:
DndScope
DndDraggable
DndDroppable
DndDragOverlay
onDragEndprovides enough data for user to move tasks manually.event.active.datais the task.event.over.datais the column or target task.- Basic demo does not require experimental API.
As an advanced developer, I want to try multi-container sorting for Kanban while accepting that the API is experimental.
- API is annotated with
@experimental. SortableContainermodel exists.SortableMoveDetailsevent exists.- Docs clearly state that this API is not stable yet.
- Experimental example exists.
- Breaking changes to experimental API do not affect stable API.
Bring the package to a public, usable, production-oriented release quality.
As an app developer, I want to know when I misconfigure the library instead of having it fail silently.
- Debug asserts for:
duplicate DndId
DndDraggable outside DndScope
DndDroppable outside DndScope
SortableItem outside SortableScope
ID in SortableScope does not exist in children
- Release mode does not crash unnecessarily.
DndDiagnosticsConfigexists.onWarningcallback exists.- Warning messages are clear and actionable.
As a maintainer, I want benchmarks to prevent performance regressions.
- Benchmark/example with 200 visible draggable items exists.
- Dragging does not rebuild the entire app on every move.
- Collision does not run more than once per frame.
- Overlay position updates smoothly.
- Debug counters exist for:
measure count
collision count
critical widget rebuild count
- Performance guide exists in docs.
As a package maintainer, I want to ensure the package runs on mobile, web, and desktop.
- CI runs analyze/test.
- Example compiles for Android.
- Example compiles for iOS if macOS CI is available.
- Example compiles for Web.
- Example compiles for macOS/Windows/Linux at least through a local/manual checklist.
- README clearly states platform support.
As a new developer, I want to read the docs and start using the library within a few minutes.
Docs include at minimum:
Getting Started
Basic Draggable/Droppable
Drag Overlay
Drag Handle
Collision Detection
Modifiers
Sensors
Auto-scroll
Sortable List
Sortable Grid
Kanban Example
Accessibility
Performance Guide
Troubleshooting
Migration Guide
- Each docs page has a code sample.
- Code samples compile.
- README includes GIF/screenshot demo.
- API docs generate without errors.
Goal: API skeleton + core tests.
Includes:
- repo setup
- dnd_kit_core
- DndId
- geometry
- state machine
- collision algorithms
- modifier contract
- initial docs
Not production-ready.
Goal: basic Flutter adapter.
Includes:
- DndScope
- DndController
- DndDraggable
- DndDroppable
- pointer sensor
- basic measuring
- basic drag events
- basic example
Goal: visual state + overlay + handles.
Includes:
- DndDragHandle
- DndDragOverlay
- draggable builder state
- droppable builder state
- distance activation
- long press activation
Goal: collision, modifiers, and measuring hardening.
Includes:
- custom collision detector
- built-in collision detectors
- built-in modifiers
- measuring cache
- scroll/layout invalidation
Goal: sortable preset.
Includes:
- SortableScope
- SortableItem
- vertical list
- horizontal list
- grid
- sortable examples
Goal: auto-scroll + keyboard.
Includes:
- auto-scroll
- keyboard sensor
- semantics
- accessibility docs
Goal: Kanban showcase.
Includes:
- Kanban example
- custom collision example
- experimental multi-container model
Release only when:
- stable API list is frozen
- core test coverage is solid
- examples run correctly
- docs are good enough for public usage
- no known critical bugs remain
- performance baseline is acceptable
- breaking API has been reviewed carefully
DndId
DndPoint
DndSize
DndRect
DndTransform
DndScope
DndController
DndDraggable
DndDroppable
DndDragHandle
DndDragOverlay
DndCollisionDetector
DndCollisionDetectors
DndModifier
DndSensor
DndActivationConstraint
DndDragStartEvent
DndDragMoveEvent
DndDragOverEvent
DndDragEndEvent
DndDragCancelEvent
SortableScope
SortableItem
SortableStrategies.verticalList
SortableStrategies.horizontalList
SortableStrategies.grid
SortableScope.multi
SortableContainer
VirtualSortableAdapter
NestedSortableScope
Debug visualizer
Devtools overlay
Before publishing any package:
dart format .
dart analyze
dart test
flutter test
melos run test
melos run analyze
melos run example:build:web
Before 1.0.0:
- No public API is missing dartdoc.
- No example has analyze errors.
- No TODO remains in stable API.
- No experimental API is exported as stable without @experimental.
- No unnecessary dependency is included.
- CHANGELOG is complete.
- README includes basic API usage and links to docs.
This is one of the biggest risks.
Mitigation:
- normalize all rects into global coordinates;
- test with nested Transform/ScrollView;
- provide debug overlay for rect visualization;
- never mix local/global coordinates during collision.
Especially important on mobile.
Mitigation:
- use long press by default for touch;
- use distance constraint for mouse/pointer;
- support drag handles;
- use activation tolerance;
- avoid claiming gestures too early.
Mitigation:
- use controller/listenable instead of setState on the whole scope;
- update overlay independently;
- let individual draggable/droppable widgets subscribe to their own state;
- throttle collision to frame boundaries.
Mitigation:
- implement vertical/horizontal list first;
- stabilize grid strategy only after enough tests;
- do not support masonry as stable in v1.
Mitigation:
- design events with from/to container fields from the beginning;
- keep multi-container API experimental;
- first Kanban demo should use generic DndDraggable/DndDroppable.
dnd_kit is considered production-ready v1 when:
- Basic drag/drop works reliably on mobile/web/desktop.
- Drag overlay is stable.
- Custom collision detector works.
- Modifiers work.
- Auto-scroll works for common cases.
- Sortable vertical list is stable.
- Sortable horizontal list is stable.
- Sortable grid is stable.
- Basic keyboard accessibility flow exists.
- Kanban demo works.
- Docs are sufficient for users to adopt the library.
- Stable API is clearly documented.
- Experimental API is clearly marked.
- Core logic has meaningful test coverage.
- No external state management dependency is used.
The best architecture for this library is:
Generic DnD engine first
Flutter adapter second
Sortable as preset
Kanban as showcase
Multi-container as experimental
Native OS drag/drop as future package
With this design, dnd_kit will not merely become a better ReorderableListView. It can become a foundation package for complex Flutter UIs, especially web/desktop admin tools, Kanban boards, builders/editors, dashboards, and drag-heavy productivity apps.