VooKanban

A highly customizable Kanban board widget for Flutter with drag-and-drop, swimlanes, WIP limits, and extensive theming.

pub package style: flutter_lints

Features

  • Generic Card Support: Type-safe cards with arbitrary data <T>
  • Drag-and-Drop: Move cards within and between lanes with smooth animations
  • Swimlanes: Horizontal groupings with filter functions and collapsible rows
  • WIP Limits: Visual indicators when approaching or exceeding lane limits
  • Selection: Single and multi-select modes with Ctrl/Cmd+Click support
  • Keyboard Navigation: Arrow keys, Enter, Escape for accessibility
  • Undo/Redo: Full history stack with Ctrl+Z / Ctrl+Y shortcuts
  • Theming: Material 3 integration with light/dark presets and full customization
  • Responsive: Tab-based mobile layout, horizontal scrolling desktop layout
  • Serialization: toJson() / fromJson() for state persistence

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  voo_kanban: ^0.0.2

Then run:

flutter pub get

Quick Start

import 'package:flutter/material.dart';
import 'package:voo_kanban/voo_kanban.dart';

class MyKanbanBoard extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return VooKanbanBoard<String>(
      lanes: [
        KanbanLane(
          id: 'todo',
          title: 'To Do',
          cards: [
            KanbanCard(id: '1', data: 'Task 1', laneId: 'todo', index: 0),
            KanbanCard(id: '2', data: 'Task 2', laneId: 'todo', index: 1),
          ],
        ),
        KanbanLane(
          id: 'doing',
          title: 'In Progress',
          wipLimit: 3,
        ),
        KanbanLane(
          id: 'done',
          title: 'Done',
        ),
      ],
      cardBuilder: (context, card, isSelected) {
        return Card(
          color: isSelected ? Theme.of(context).colorScheme.primaryContainer : null,
          child: Padding(
            padding: const EdgeInsets.all(12),
            child: Text(card.data),
          ),
        );
      },
      onCardMoved: (card, fromLane, toLane, index) {
        print('Moved ${card.data} from $fromLane to $toLane');
      },
    );
  }
}

Usage

Basic Board with Custom Data

// Define your task model
class Task {
  final String title;
  final String description;
  final Priority priority;

  const Task({
    required this.title,
    required this.description,
    required this.priority,
  });
}

enum Priority { low, medium, high }

// Create the board
VooKanbanBoard<Task>(
  lanes: [
    KanbanLane<Task>(
      id: 'backlog',
      title: 'Backlog',
      icon: Icons.inbox,
      cards: [
        KanbanCard<Task>(
          id: 'task-1',
          data: Task(
            title: 'Research competitors',
            description: 'Analyze market landscape',
            priority: Priority.low,
          ),
          laneId: 'backlog',
          index: 0,
        ),
      ],
    ),
    KanbanLane<Task>(
      id: 'in-progress',
      title: 'In Progress',
      icon: Icons.pending_actions,
      color: Colors.blue,
      wipLimit: 3,
    ),
    KanbanLane<Task>(
      id: 'done',
      title: 'Done',
      icon: Icons.check_circle,
      color: Colors.green,
    ),
  ],
  cardBuilder: (context, card, isSelected) => TaskCard(task: card.data),
  onCardMoved: (card, fromLane, toLane, index) {
    // Handle card movement
  },
  onCardTap: (card) {
    // Handle card tap
  },
)

Using KanbanController

For full control over board state, use KanbanController:

class _MyBoardState extends State<MyBoard> {
  late KanbanController<Task> _controller;

  @override
  void initState() {
    super.initState();
    _controller = KanbanController<Task>(
      initialState: KanbanState<Task>(
        lanes: _createLanes(),
        config: const KanbanConfig(
          enableUndo: true,
          enableKeyboardNavigation: true,
          showCardCount: true,
          showWipLimitIndicators: true,
        ),
      ),
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return ListenableBuilder(
      listenable: _controller,
      builder: (context, _) {
        return Column(
          children: [
            // Undo/Redo toolbar
            Row(
              children: [
                IconButton(
                  icon: const Icon(Icons.undo),
                  onPressed: _controller.canUndo ? _controller.undo : null,
                ),
                IconButton(
                  icon: const Icon(Icons.redo),
                  onPressed: _controller.canRedo ? _controller.redo : null,
                ),
              ],
            ),
            Expanded(
              child: VooKanbanBoard<Task>(
                controller: _controller,
                lanes: _controller.lanes,
                cardBuilder: _buildCard,
              ),
            ),
          ],
        );
      },
    );
  }
}

Controller Operations

// Move a card
_controller.moveCard(
  cardId: 'task-1',
  toLaneId: 'in-progress',
  toIndex: 0,
);

// Add a card
_controller.addCard(KanbanCard<Task>(
  id: 'new-task',
  data: Task(title: 'New Task', ...),
  laneId: 'backlog',
));

// Remove a card
_controller.removeCard('task-1');

// Selection
_controller.selectCard('task-1');
_controller.selectCard('task-2', addToSelection: true);
_controller.clearSelection();

// Undo/Redo
_controller.undo();
_controller.redo();

// Serialization
final json = _controller.toJson(dataToJson: (task) => task.toJson());
_controller.fromJson(json, dataFromJson: Task.fromJson);

Swimlanes

Group cards horizontally using swimlanes with filter functions:

VooKanbanBoard<Task>(
  lanes: _lanes,
  swimlanes: [
    KanbanSwimlane<Task>(
      id: 'high-priority',
      title: 'High Priority',
      filter: (card) => card.data.priority == Priority.high,
    ),
    KanbanSwimlane<Task>(
      id: 'medium-priority',
      title: 'Medium Priority',
      filter: (card) => card.data.priority == Priority.medium,
    ),
    KanbanSwimlane<Task>(
      id: 'low-priority',
      title: 'Low Priority',
      filter: (card) => card.data.priority == Priority.low,
    ),
  ],
  swimlaneHeaderBuilder: (context, swimlane) {
    return Text(swimlane.title, style: Theme.of(context).textTheme.titleMedium);
  },
)

Theming

VooKanban integrates with Material 3 and provides extensive theming options:

// Automatic Material 3 theming
VooKanbanBoard<Task>(
  theme: KanbanTheme.fromContext(context),
  // ...
)

// Light/Dark presets
VooKanbanBoard<Task>(
  theme: KanbanTheme.light(),
  // or
  theme: KanbanTheme.dark(),
)

// Custom theme
VooKanbanBoard<Task>(
  theme: KanbanTheme.fromContext(context).copyWith(
    boardBackgroundColor: Colors.grey[100],
    laneBackgroundColor: Colors.white,
    cardBackgroundColor: Colors.white,
    wipWarningColor: Colors.orange,
    wipExceededColor: Colors.red,
    laneBorderRadius: BorderRadius.circular(16),
    cardBorderRadius: BorderRadius.circular(12),
  ),
)

Configuration Options

const KanbanConfig(
  // Drag behavior
  dragMode: DragMode.betweenLanes, // or DragMode.withinLane, DragMode.disabled

  // Selection
  selectionMode: SelectionMode.single, // or SelectionMode.multiple

  // Lane behavior
  allowLaneReorder: true,
  allowSwimlaneReorder: false,

  // Undo/Redo
  enableUndo: true,
  maxUndoSteps: 50,

  // Navigation
  enableKeyboardNavigation: true,

  // Visual options
  showWipLimitIndicators: true,
  showCardCount: true,
  enableAnimations: true,
  animationDuration: Duration(milliseconds: 300),

  // Layout
  defaultLaneWidth: 300,
  minLaneWidth: 250,
  maxLaneWidth: 400,
  laneSpacing: 12,
  cardSpacing: 8,
)

WIP Limits

Configure Work-In-Progress limits per lane:

KanbanLane<Task>(
  id: 'in-progress',
  title: 'In Progress',
  wipLimit: 3, // Maximum 3 cards
  cards: [...],
)

// Handle WIP limit exceeded
VooKanbanBoard<Task>(
  onWipLimitExceeded: (lane, currentCount) {
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(
        content: Text('WIP limit exceeded: $currentCount/${lane.wipLimit}'),
        backgroundColor: Colors.orange,
      ),
    );
  },
)

Custom Builders

VooKanbanBoard<Task>(
  // Custom card rendering
  cardBuilder: (context, card, isSelected) {
    return TaskCardWidget(task: card.data, isSelected: isSelected);
  },

  // Custom lane header
  laneHeaderBuilder: (context, lane) {
    return Row(
      children: [
        Icon(lane.icon),
        Text(lane.title),
        Chip(label: Text('${lane.cardCount}')),
      ],
    );
  },

  // Custom lane footer
  laneFooterBuilder: (context, lane) {
    return TextButton(
      onPressed: () => _addCard(lane.id),
      child: const Text('+ Add Card'),
    );
  },

  // Empty lane placeholder
  emptyLaneBuilder: (context, lane) {
    return Center(
      child: Text('Drop cards here', style: TextStyle(color: Colors.grey)),
    );
  },
)

Callbacks

VooKanbanBoard<Task>(
  // Card moved between lanes or reordered
  onCardMoved: (card, fromLaneId, toLaneId, newIndex) { },

  // Card tapped
  onCardTap: (card) { },

  // Card long pressed
  onCardLongPress: (card) { },

  // Selection changed
  onCardsSelected: (selectedIds) { },

  // Lanes reordered
  onLaneReordered: (oldIndex, newIndex) { },

  // WIP limit exceeded
  onWipLimitExceeded: (lane, currentCount) { },

  // Undo/Redo triggered
  onUndo: () { },
  onRedo: () { },
)

Keyboard Shortcuts

Shortcut Action
Arrow Up/Down Navigate between cards
Enter/Space Select focused card
Escape Clear selection and focus
Ctrl/Cmd + Z Undo
Ctrl/Cmd + Shift + Z Redo
Ctrl/Cmd + Y Redo
Ctrl/Cmd + Click Multi-select cards

Serialization

Save and restore board state:

// Save state
final json = controller.toJson(
  dataToJson: (task) => {
    'title': task.title,
    'description': task.description,
    'priority': task.priority.name,
  },
);

// Restore state
controller.fromJson(
  json,
  dataFromJson: (data) => Task(
    title: data['title'],
    description: data['description'],
    priority: Priority.values.byName(data['priority']),
  ),
);

Platform Support

Platform Support
Android Yes
iOS Yes
Web Yes
macOS Yes
Windows Yes
Linux Yes

Dependencies

  • voo_ui_core: ^0.1.3
  • voo_responsive: ^0.1.4
  • voo_tokens: ^0.0.8
  • voo_motion: ^0.0.2
  • equatable: ^2.0.5

Example App

Check out the example app for comprehensive demonstrations:

cd example
flutter run

The example includes:

  • Basic board setup
  • Drag and drop interactions
  • Swimlane grouping
  • Theming customization

License

This package is part of the VooFlutter monorepo. See the root LICENSE file for details.


Built by VooStack

Need help with Flutter development or custom Kanban solutions?

Contact Us

VooStack builds enterprise Flutter applications and developer tools. We're here to help with your next project.

Libraries

voo_kanban
A highly customizable Kanban board widget for Flutter.