# How to Develop a Custom Calendar View with Drag and Drop Events in FlutterFlow

- Tool: FlutterFlow
- Difficulty: Beginner
- Time required: 30-40 min
- Compatibility: FlutterFlow Pro+ (Custom Widgets with Dart code)
- Last updated: March 2026

## TL;DR

Build an interactive calendar using a Custom Widget that wraps table_calendar with LongPressDraggable and DragTarget widgets. Users long-press an event to drag it to a different date for rescheduling, with ghost previews showing the dragged item and target date highlights. Drop triggers a Firestore update to the event's date field. Add drag-to-resize for event duration by dragging the bottom edge to extend the endTime. All Firestore writes happen only on drop, not during drag movement, to avoid excessive writes.

## Building a Drag-and-Drop Calendar in FlutterFlow

Read-only calendars are frustrating when users need to reschedule. This tutorial builds an interactive calendar where events can be dragged to new dates and resized by dragging their edges. The Custom Widget combines table_calendar with Flutter's drag-and-drop system for a native feel.

## Before you start

- FlutterFlow project with Firestore events collection
- Events collection with date, title, and duration fields
- table_calendar package added to pubspec dependencies
- Basic understanding of FlutterFlow Custom Widgets

## Step-by-step guide

### 1. Create the Firestore schema for calendar events

In Firestore, create an events collection with fields: title (String), date (Timestamp), startTime (Timestamp), endTime (Timestamp), color (String, hex code), userId (String), description (String, optional). The date field stores the calendar day for query filtering. The startTime and endTime fields define the event's time range within that day. The color field lets users categorize events visually. Query events by userId and date range to load only the visible month's events.

**Expected result:** Firestore has an events collection with date, time range, and color fields for calendar display.

### 2. Build the Custom Widget with table_calendar and drag support

Create a Custom Widget called DraggableCalendar. Inside, use TableCalendar from the table_calendar package as the base. Override the calendarBuilders to make each day cell a DragTarget that accepts event data. For event markers under each date, build them as LongPressDraggable widgets so users can long-press to start dragging. The LongPressDraggable shows a ghost copy of the event chip while dragging. The DragTarget for each day cell highlights with a border color when an event is dragged over it. Pass events as a widget parameter (List of event maps) and an Action Parameter callback onEventMoved(eventId, newDate).

```
// Custom Widget: DraggableCalendar (simplified core)
import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';

class DraggableCalendar extends StatefulWidget {
  final List<Map<String, dynamic>> events;
  final Function(String eventId, DateTime newDate) onEventMoved;
  final double width;
  final double height;

  const DraggableCalendar({
    required this.events,
    required this.onEventMoved,
    required this.width,
    required this.height,
  });

  @override
  State<DraggableCalendar> createState() => _DraggableCalendarState();
}

class _DraggableCalendarState extends State<DraggableCalendar> {
  DateTime _focusedDay = DateTime.now();
  DateTime? _hoveredDay;

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: widget.width,
      height: widget.height,
      child: TableCalendar(
        firstDay: DateTime.utc(2024, 1, 1),
        lastDay: DateTime.utc(2030, 12, 31),
        focusedDay: _focusedDay,
        calendarBuilders: CalendarBuilders(
          defaultBuilder: (ctx, day, focused) {
            return _buildDayCell(day);
          },
          markerBuilder: (ctx, day, events) {
            return _buildEventMarkers(day);
          },
        ),
      ),
    );
  }

  Widget _buildDayCell(DateTime day) {
    return DragTarget<Map<String, dynamic>>(
      onWillAcceptWithDetails: (details) {
        setState(() => _hoveredDay = day);
        return true;
      },
      onLeave: (_) => setState(() => _hoveredDay = null),
      onAcceptWithDetails: (details) {
        widget.onEventMoved(details.data['id'], day);
        setState(() => _hoveredDay = null);
      },
      builder: (ctx, candidates, rejects) {
        return Container(
          decoration: BoxDecoration(
            border: _hoveredDay == day
                ? Border.all(color: Colors.blue, width: 2)
                : null,
          ),
          child: Center(child: Text('${day.day}')),
        );
      },
    );
  }

  Widget _buildEventMarkers(DateTime day) {
    final dayEvents = widget.events.where(
      (e) => isSameDay(DateTime.parse(e['date']), day)
    ).toList();
    return Wrap(
      children: dayEvents.map((event) {
        return LongPressDraggable<Map<String, dynamic>>(
          data: event,
          feedback: Material(
            child: Container(
              padding: const EdgeInsets.all(4),
              color: Colors.blue.withOpacity(0.7),
              child: Text(event['title'],
                  style: const TextStyle(color: Colors.white)),
            ),
          ),
          childWhenDragging: Opacity(
            opacity: 0.3,
            child: _eventChip(event),
          ),
          child: _eventChip(event),
        );
      }).toList(),
    );
  }

  Widget _eventChip(Map<String, dynamic> event) {
    return Container(
      margin: const EdgeInsets.all(1),
      padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
      decoration: BoxDecoration(
        color: Colors.blue,
        borderRadius: BorderRadius.circular(4),
      ),
      child: Text(event['title'] ?? '',
          style: const TextStyle(color: Colors.white, fontSize: 10)),
    );
  }
}
```

**Expected result:** A calendar widget where events can be long-pressed and dragged to different dates.

### 3. Handle the drop event to update Firestore

In the parent page, add the DraggableCalendar Custom Widget and bind the onEventMoved callback to an Action Flow. When the callback fires with an eventId and newDate, update the Firestore events document: set the date field to the new date Timestamp, and adjust startTime and endTime to the same time-of-day on the new date. Show a SnackBar confirming the reschedule with an Undo option that reverts to the original date. This ensures the Firestore write only happens once on drop, not during the drag movement across intermediate dates.

**Expected result:** Dropping an event on a new date updates the Firestore document and shows a confirmation with Undo.

### 4. Add visual feedback with ghost preview and target highlighting

In the Custom Widget, the LongPressDraggable feedback parameter shows a semi-transparent copy of the event chip that follows the user's finger. The childWhenDragging parameter reduces the original chip to 30% opacity so users see where the event came from. The DragTarget builder checks if a candidate is hovering and adds a blue border highlight to the day cell. When the drag enters a day, the border appears. When it leaves, the border disappears. This combination of ghost, dimmed source, and highlighted target gives users clear visual feedback throughout the drag operation.

**Expected result:** Dragging shows a ghost event chip, dims the source position, and highlights the target date cell.

### 5. Implement drag-to-resize for changing event duration

For events displayed in a day detail view (a vertical timeline showing events as positioned Containers), add a GestureDetector on the bottom edge of each event Container. When the user drags this handle downward, increase the event's endTime in proportion to the vertical distance dragged. Use a Page State variable resizingEndTime to track the new end time during the drag and update the Container height in real-time. On drag end (onPanEnd), write the final endTime to Firestore. This allows users to extend or shorten events by dragging their bottom edge up or down.

**Expected result:** Users drag the bottom edge of an event to change its duration, with the change saved on release.

## Complete code example

File: `FlutterFlow Drag-and-Drop Calendar`

```text
FIRESTORE SCHEMA:
  events (collection):
    title: String
    date: Timestamp (calendar day)
    startTime: Timestamp
    endTime: Timestamp
    color: String (hex code)
    userId: String
    description: String (optional)

CUSTOM WIDGET: DraggableCalendar
  Parameters:
    events: List<Map> (event data)
    onEventMoved: Action Parameter (eventId, newDate)
  
  TableCalendar:
    calendarBuilders:
      defaultBuilder → DragTarget per day cell
        onAcceptWithDetails → call onEventMoved callback
        Highlight border on hover
      markerBuilder → LongPressDraggable per event
        feedback: semi-transparent event chip (ghost)
        childWhenDragging: dimmed original chip (30% opacity)

PAGE: CalendarPage
  Backend Query: events where userId == currentUser
    filtered by visible month date range
  DraggableCalendar Custom Widget
    events: query results
    onEventMoved: Action Flow
      → Update Firestore event document (date, startTime, endTime)
      → Show SnackBar with Undo option

DAY DETAIL VIEW:
  Vertical timeline (positioned Containers per event)
  Each event Container:
    Height proportional to duration
    Bottom edge: GestureDetector for drag-to-resize
      onPanUpdate → update Page State resizingEndTime
      onPanEnd → write final endTime to Firestore

VISUAL FEEDBACK:
  LongPressDraggable feedback → ghost chip follows finger
  childWhenDragging → source chip at 30% opacity
  DragTarget → blue border highlight on hover
  SnackBar → 'Event moved to {newDate}' with Undo
```

## Common mistakes

- **Writing to Firestore during drag movement across intermediate dates** — If a user drags slowly across five dates, each intermediate date triggers a write. This wastes Firestore operations and creates flickering as the event document is updated mid-drag. Fix: Only write to Firestore on the drop event (onAcceptWithDetails), never during drag movement. The visual feedback is handled locally in widget state.
- **Not providing visual feedback during the drag operation** — Without a ghost preview and target highlighting, users cannot tell what they are dragging or where it will land. The interaction feels broken and imprecise. Fix: Use LongPressDraggable's feedback for a ghost chip, childWhenDragging for a dimmed source, and DragTarget's builder for border highlighting on hover.
- **Forgetting to adjust startTime and endTime when moving an event to a new date** — If you only update the date field but leave startTime and endTime on the original date, the event appears on the new date but its time data is inconsistent. Time-based queries break. Fix: When updating the date, also update startTime and endTime to the same hour and minute on the new date. Preserve the time-of-day while changing the calendar day.

## Best practices

- Write to Firestore only on drop, never during drag movement
- Show ghost preview, dimmed source, and highlighted target for clear drag feedback
- Adjust all date-related fields (date, startTime, endTime) when rescheduling
- Add an Undo SnackBar after each move so users can revert accidental drops
- Load only the visible month's events to keep the widget responsive
- Use color-coded event chips for quick visual category identification
- Debounce duration resize updates if you need real-time preview during drag

## Frequently asked questions

### Can I restrict which events are draggable?

Yes. Wrap the LongPressDraggable conditionally: only make events draggable if the current user is the event creator or if the event status allows editing. Non-draggable events render as static chips without drag behavior.

### Does this work on web as well as mobile?

Yes. LongPressDraggable supports both touch (long-press on mobile) and mouse (click-and-hold on web). The interaction adapts to the input method automatically.

### How do I handle recurring events?

Store a recurrence rule (weekly, monthly, custom) on the event document. When a recurring event is dragged, ask the user whether to move only this instance or all future occurrences. For single-instance changes, create an exception document.

### Can I snap the drop to specific time slots?

Yes. In the day detail view, round the dropped position to the nearest 15 or 30-minute increment before updating the time fields. This prevents events from landing at odd times like 2:07 PM.

### What happens if two users drag the same event simultaneously?

The last write wins in Firestore. To prevent conflicts, use a Firestore transaction that reads the current date before writing. If the date changed since the drag started, show a conflict message.

### Can RapidDev help build advanced calendar features?

Yes. RapidDev can build interactive calendars with drag-and-drop, recurring events, multi-user scheduling, conflict detection, and integration with external calendar services like Google Calendar.

---

Source: https://www.rapidevelopers.com/flutterflow-tutorials/how-to-develop-a-custom-calendar-view-with-drag-and-drop-events-in-flutterflow
© RapidDev — https://www.rapidevelopers.com/flutterflow-tutorials/how-to-develop-a-custom-calendar-view-with-drag-and-drop-events-in-flutterflow
