TimelineKit

Interaction

Drag & Drop

Cards, columns, and swimlanes can all be reordered by dragging. Cards can be moved between columns and swimlanes. A placeholder shows the drop target while dragging.

Card Drag

// Intercept card move (before mutation — cancelable)
board.events.cardMoving$.subscribe(args => {
  // args: { card, fromColumnId, toColumnId, toIndex, toSwimlaneId, cancel }

  // Prevent moving to a specific column
  if (args.toColumnId === 'archived-column-id') {
    args.cancel = true;
  }
});

// React after card was moved by drag (after mutation)
board.events.cardMovedByDrag$.subscribe(args => {
  // args: { card, fromColumnId, toColumnId, toIndex }
  console.log('Card moved from', args.fromColumnId, 'to', args.toColumnId);
});

Column Drag

// Intercept column reorder (cancelable)
board.events.columnMoving$.subscribe(args => {
  // args: { column, fromIndex, toIndex, cancel }
  args.cancel = true; // prevent reorder
});

// React after column was moved
board.events.columnMovedByDrag$.subscribe(args => {
  // args: { column, fromIndex, toIndex }
});

Swimlane Drag

// Intercept swimlane reorder (cancelable)
board.events.swimlaneMoving$.subscribe(args => {
  // args: { swimlane, fromIndex, toIndex, cancel }
});

// React after swimlane was moved
board.events.swimlaneMovedByDrag$.subscribe(args => {
  // args: { swimlane, fromIndex, toIndex }
});

Selection

Click a card to select it. Hold Ctrl/Cmd to multi-select. The selected cards are available via the selectedCards property.

// Get selected cards
const selected = board.selectedCards;
const single = board.selectedCard; // last selected, or null

// Set selection programmatically
board.selectedCards = [card1, card2];
board.selectedCard = card1;

// Listen for selection changes
board.events.selectedCardsChanged$.subscribe(cards => {
  console.log('Selected:', cards.map(c => c.title));
});

// Listen for hover changes
board.events.hoveredCardChanged$.subscribe(card => {
  console.log('Hovered:', card?.title ?? 'none');
});

Context Menu

Right-click events let you build custom context menus for cards, columns, swimlanes, empty column areas, and the board background.

// Right-click on a card
board.events.cardContextMenu$.subscribe(({ card, domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Edit', action: () => editCard(card) },
    { label: 'Delete', action: () => board.data.removeCard(card) },
    { label: 'Move to Done', action: () => {
      board.data.moveCard(card, 'done-column-id', 0);
    }},
  ]);
});

// Right-click on a column header
board.events.columnContextMenu$.subscribe(({ column, domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Rename', action: () => renameColumn(column) },
    { label: 'Collapse', action: () => { column.collapsed = true; } },
    { label: 'Delete', action: () => board.data.removeColumn(column) },
  ]);
});

// Right-click on a swimlane header
board.events.swimlaneContextMenu$.subscribe(({ swimlane, domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Collapse', action: () => { swimlane.collapsed = true; } },
    { label: 'Delete', action: () => board.data.removeSwimlane(swimlane) },
  ]);
});

// Right-click on empty area within a column
board.events.columnEmptyContextMenu$.subscribe(({ column, swimlane, domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Add Card', action: () => {
      const card = new KanbanCard(null, column.id);
      card.title = 'New Card';
      if (swimlane) card.swimlaneId = swimlane.id;
      board.data.addCard(card);
    }},
  ]);
});

// Right-click on the board background (no column/card)
board.events.boardContextMenu$.subscribe(({ domEvent }) => {
  domEvent.preventDefault();
  showContextMenu(domEvent.clientX, domEvent.clientY, [
    { label: 'Add Column', action: () => { /* ... */ } },
    { label: 'Add Swimlane', action: () => { /* ... */ } },
  ]);
});

Click Events

Click and double-click events are available for cards, columns, swimlanes, and empty column areas.

// Card click / double-click
board.events.cardClick$.subscribe(({ card, domEvent }) => {
  console.log('Clicked:', card.title);
});
board.events.cardDblClick$.subscribe(({ card, domEvent }) => {
  openCardEditor(card);
});

// Column header click / double-click
board.events.columnClick$.subscribe(({ column, domEvent }) => { /* ... */ });
board.events.columnDblClick$.subscribe(({ column, domEvent }) => { /* ... */ });

// Swimlane header click / double-click
board.events.swimlaneClick$.subscribe(({ swimlane, domEvent }) => { /* ... */ });
board.events.swimlaneDblClick$.subscribe(({ swimlane, domEvent }) => { /* ... */ });

// Empty column area click / double-click
board.events.columnEmptyClick$.subscribe(({ column, swimlane, domEvent }) => { /* ... */ });
board.events.columnEmptyDblClick$.subscribe(({ column, swimlane, domEvent }) => {
  // Double-click to quickly add a card
  const card = new KanbanCard(null, column.id);
  card.title = 'New Card';
  if (swimlane) card.swimlaneId = swimlane.id;
  board.data.addCard(card);
});

Keyboard Navigation

The board supports keyboard navigation and shortcuts. Use arrow keys to move between cards, Enter to select, and Delete to remove.

Filtering

Apply a filter function to show only cards matching certain criteria. Filtered-out cards are hidden from the board but remain in the data model.

// Show only high-priority cards
board.data.filter = (card) => {
  return card.priority === 'high' || card.priority === 'critical';
};

// Filter by tag
board.data.filter = (card) => card.tags.includes('backend');

// Filter by assignee
board.data.filter = (card) => card.assigneeIds.includes('alice');

// Clear the filter
board.data.filter = null;

Card Deletion

Intercept card deletion to show a confirmation dialog or prevent deletion of certain cards.

board.events.cardDeleting$.subscribe(args => {
  // args: { card, cancel }
  if (args.card.priority === 'critical') {
    args.cancel = true; // prevent deletion of critical cards
  }
});

Undo & Redo

All operations (card moves, additions, deletions, property changes) are tracked and can be undone/redone.

board.undo();
board.redo();

// Check if undo/redo is available
board.canUndo$.subscribe(canUndo => {
  undoButton.disabled = !canUndo;
});
board.canRedo$.subscribe(canRedo => {
  redoButton.disabled = !canRedo;
});

// Clear undo history
board.clearUndoHistory();

// Batch multiple operations into a single undo step
board.beginBatch();
board.data.addCard(card1);
board.data.addCard(card2);
board.data.addCard(card3);
board.endBatch();
// Now a single undo() removes all three cards

// Listen for undo/redo events
board.events.undone$.subscribe(() => console.log('Undone'));
board.events.redone$.subscribe(() => console.log('Redone'));

Copy & Paste

Copy, cut, and paste cards between columns and swimlanes.

// Copy selected cards (or pass specific cards)
board.copyCards();
board.copyCards([card1, card2]);

// Cut selected cards
board.cutCards();

// Paste into a target column (and optionally a swimlane)
board.pasteCards('target-column-id');
board.pasteCards('target-column-id', 'target-swimlane-id');