image

13 Dec 2025

9K

35K

Building an Interactive Calendar Widget in Flutter

Interactive calendar widgets are an indispensable component in modern applications, providing users with an intuitive way to view dates, schedule events, and manage their time. From booking systems and task managers to personal organizers, a well-implemented calendar significantly enhances user experience. Flutter, with its rich set of widgets and declarative UI approach, makes creating such interactive elements a streamlined process. This article will guide you through building a dynamic and customizable calendar widget using Flutter, leveraging the popular table_calendar package.

Prerequisites

Before diving in, ensure you have a basic understanding of Flutter development, including:

  • Flutter SDK installed and configured.
  • Familiarity with Dart programming language.
  • Knowledge of StatelessWidget and StatefulWidget concepts.

Integrating table_calendar

The table_calendar package is a feature-rich, highly customizable Flutter calendar widget. To begin, add it to your pubspec.yaml file under the dependencies section:


dependencies:
  flutter:
    sdk: flutter
  table_calendar: ^3.0.9 # Use the latest version

After adding the dependency, run flutter pub get in your terminal to fetch the package.

Building the Basic Calendar Widget

Let's start by creating a simple stateful widget that displays a basic calendar. We'll use StatefulWidget because our calendar will need to manage its internal state, such as the currently selected or focused day.


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

class CalendarPage extends StatefulWidget {
  const CalendarPage({super.key});

  @override
  State<CalendarPage> createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Calendar'),
      ),
      body: TableCalendar(
        firstDay: DateTime.utc(2010, 10, 16),
        lastDay: DateTime.utc(2030, 3, 14),
        focusedDay: _focusedDay,
        calendarFormat: _calendarFormat,
        selectedDayPredicate: (day) {
          // Use `selectedDayPredicate` to determine which days should be marked as selected
          return isSameDay(_selectedDay, day);
        },
        onDaySelected: (selectedDay, focusedDay) {
          if (!isSameDay(_selectedDay, selectedDay)) {
            setState(() {
              _selectedDay = selectedDay;
              _focusedDay = focusedDay; // update `_focusedDay` as well
            });
          }
        },
        onFormatChanged: (format) {
          if (_calendarFormat != format) {
            setState(() {
              _calendarFormat = format;
            });
          }
        },
        onPageChanged: (focusedDay) {
          // No need to call setState, it will be called inside `onDaySelected`
          _focusedDay = focusedDay;
        },
      ),
    );
  }
}

In this basic setup:

  • firstDay and lastDay define the valid date range for the calendar.
  • focusedDay determines which month/year is currently displayed.
  • calendarFormat controls whether the calendar shows month, two weeks, or week view.
  • selectedDayPredicate is a callback that highlights a day if it matches the _selectedDay state.
  • onDaySelected is triggered when a user taps a day, updating the _selectedDay and _focusedDay state variables.

Enhancing Interactivity: Date Selection and Focus

The core of an interactive calendar lies in its ability to respond to user input. The onDaySelected callback is crucial for this. When a day is selected, we update the _selectedDay variable and then trigger a UI rebuild using setState(). The selectedDayPredicate then uses this updated state to visually highlight the chosen day.

The _focusedDay variable ensures that the calendar view stays consistent, either on the initially focused month or updates when the user navigates between months or selects a day in a different month.

Customizing the Calendar's Appearance

table_calendar offers extensive customization options to match your application's theme. You can customize the header, individual day cells, event markers, and more.


// Inside your TableCalendar widget's build method:
TableCalendar(
  // ... (previous properties)
  headerStyle: const HeaderStyle(
    formatButtonVisible: false, // Hide format change button
    titleCentered: true,
    titleTextStyle: TextStyle(fontSize: 17.0, fontWeight: FontWeight.bold),
    leftChevronIcon: Icon(Icons.chevron_left, color: Colors.blue),
    rightChevronIcon: Icon(Icons.chevron_right, color: Colors.blue),
  ),
  calendarStyle: CalendarStyle(
    selectedDecoration: BoxDecoration(
      color: Colors.blueAccent,
      shape: BoxShape.circle,
    ),
    todayDecoration: BoxDecoration(
      color: Colors.blue.withOpacity(0.5),
      shape: BoxShape.circle,
    ),
    defaultTextStyle: TextStyle(color: Colors.black87),
    weekendTextStyle: TextStyle(color: Colors.redAccent),
    outsideDaysVisible: false, // Hide days from previous/next month
  ),
  // ...
),

You can also define custom builders for various elements, giving you pixel-perfect control over how each part of the calendar looks:


TableCalendar(
  // ... (previous properties)
  calendarBuilders: CalendarBuilders(
    selectedBuilder: (context, date, focusedDay) {
      return Container(
        margin: const EdgeInsets.all(4.0),
        alignment: Alignment.center,
        decoration: const BoxDecoration(
          color: Colors.blue,
          shape: BoxShape.circle,
        ),
        child: Text(
          '${date.day}',
          style: const TextStyle(color: Colors.white),
        ),
      );
    },
    todayBuilder: (context, date, focusedDay) {
      return Container(
        margin: const EdgeInsets.all(4.0),
        alignment: Alignment.center,
        decoration: BoxDecoration(
          color: Colors.blue.withOpacity(0.3),
          shape: BoxShape.circle,
        ),
        child: Text(
          '${date.day}',
          style: const TextStyle(color: Colors.black),
        ),
      );
    },
    markerBuilder: (context, date, events) {
      if (events.isNotEmpty) {
        return Positioned(
          right: 1,
          bottom: 1,
          child: _buildEventsMarker(events.length),
        );
      }
      return null;
    },
  ),
)

Widget _buildEventsMarker(int count) {
  return AnimatedContainer(
    duration: const Duration(milliseconds: 300),
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      color: Colors.deepOrange[300],
    ),
    width: 16.0,
    height: 16.0,
    child: Center(
      child: Text(
        '$count',
        style: const TextStyle().copyWith(
          color: Colors.white,
          fontSize: 12.0,
        ),
      ),
    ),
  );
}

Advanced Features: Event Handling

A calendar is often used to display events. table_calendar makes this straightforward with its eventLoader property. First, you'll need a way to store your events, typically a Map<DateTime, List<dynamic>> or similar structure.


// Define a sample event class
class Event {
  final String title;
  const Event(this.title);

  @override
  String toString() => title;
}

// In your _CalendarPageState class:
late final ValueNotifier<List<Event>> _selectedEvents;
final Map<DateTime, List<Event>> kEvents = {
  DateTime.utc(2024, 1, 20): [const Event('Flutter Meetup')],
  DateTime.utc(2024, 1, 22): [const Event('Project Deadline'), const Event('Client Call')],
  DateTime.utc(2024, 1, 25): [const Event('Team Lunch')],
  DateTime.utc(2024, 2, 1): [const Event('Sprint Review')],
};

@override
void initState() {
  super.initState();
  _selectedDay = _focusedDay;
  _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
}

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

List<Event> _getEventsForDay(DateTime day) {
  return kEvents[day] ?? [];
}

// Update onDaySelected to handle events
onDaySelected: (selectedDay, focusedDay) {
  if (!isSameDay(_selectedDay, selectedDay)) {
    setState(() {
      _selectedDay = selectedDay;
      _focusedDay = focusedDay;
    });
    _selectedEvents.value = _getEventsForDay(selectedDay);
  }
},

// Add eventLoader to TableCalendar
TableCalendar(
  // ...
  eventLoader: _getEventsForDay,
  // ...
),

// Display selected events in your UI (e.g., below the calendar)
Expanded(
  child: ValueListenableBuilder<List<Event>>(
    valueListenable: _selectedEvents,
    builder: (context, value, _) {
      return ListView.builder(
        itemCount: value.length,
        itemBuilder: (context, index) {
          return Container(
            margin: const EdgeInsets.symmetric(
              horizontal: 12.0,
              vertical: 4.0,
            ),
            decoration: BoxDecoration(
              border: Border.all(),
              borderRadius: BorderRadius.circular(12.0),
            ),
            child: ListTile(
              onTap: () => print('${value[index]}'),
              title: Text('${value[index]}'),
            ),
          );
        },
      );
    },
  ),
),

Here, we introduce a ValueNotifier to efficiently update the list of events displayed below the calendar without rebuilding the entire widget tree. The _getEventsForDay function retrieves events for a given day from our kEvents map.

Complete Example

Combining all the concepts discussed, here is a more comprehensive example of an interactive calendar widget:


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

// Sample Event class
class Event {
  final String title;
  const Event(this.title);

  @override
  String toString() => title;
}

// Sample events data
final Map<DateTime, List<Event>> kEvents = {
  DateTime.utc(2024, 1, 20): [const Event('Flutter Meetup')],
  DateTime.utc(2024, 1, 22): [const Event('Project Deadline'), const Event('Client Call')],
  DateTime.utc(2024, 1, 25): [const Event('Team Lunch')],
  DateTime.utc(2024, 2, 1): [const Event('Sprint Review')],
  DateTime.utc(2024, 2, 10): [const Event('Birthday Party')],
  DateTime.utc(2024, 2, 15): [const Event('Travel Planning')],
  DateTime.utc(2024, 3, 5): [const Event('Conference')],
};

class CalendarPage extends StatefulWidget {
  const CalendarPage({super.key});

  @override
  State<CalendarPage> createState() => _CalendarPageState();
}

class _CalendarPageState extends State<CalendarPage> {
  CalendarFormat _calendarFormat = CalendarFormat.month;
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  late final ValueNotifier<List<Event>> _selectedEvents;

  @override
  void initState() {
    super.initState();
    _selectedDay = _focusedDay;
    _selectedEvents = ValueNotifier(_getEventsForDay(_selectedDay!));
  }

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

  List<Event> _getEventsForDay(DateTime day) {
    // Returns a list of [Event] for the given [day].
    return kEvents[day] ?? [];
  }

  void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
    if (!isSameDay(_selectedDay, selectedDay)) {
      setState(() {
        _selectedDay = selectedDay;
        _focusedDay = focusedDay;
      });
      _selectedEvents.value = _getEventsForDay(selectedDay);
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Interactive Calendar'),
        backgroundColor: Colors.blueAccent,
      ),
      body: Column(
        children: [
          TableCalendar<Event>(
            firstDay: DateTime.utc(2010, 10, 16),
            lastDay: DateTime.utc(2030, 3, 14),
            focusedDay: _focusedDay,
            selectedDayPredicate: (day) => isSameDay(_selectedDay, day),
            calendarFormat: _calendarFormat,
            eventLoader: _getEventsForDay,
            startingDayOfWeek: StartingDayOfWeek.monday,
            onDaySelected: _onDaySelected,
            onFormatChanged: (format) {
              if (_calendarFormat != format) {
                setState(() {
                  _calendarFormat = format;
                });
              }
            },
            onPageChanged: (focusedDay) {
              _focusedDay = focusedDay; // No setState needed
            },
            headerStyle: const HeaderStyle(
              formatButtonVisible: false,
              titleCentered: true,
              titleTextStyle: TextStyle(fontSize: 18.0, fontWeight: FontWeight.bold, color: Colors.blueAccent),
              leftChevronIcon: Icon(Icons.chevron_left, color: Colors.blueAccent),
              rightChevronIcon: Icon(Icons.chevron_right, color: Colors.blueAccent),
            ),
            calendarStyle: CalendarStyle(
              outsideDaysVisible: false,
              weekendTextStyle: const TextStyle(color: Colors.redAccent),
              selectedDecoration: const BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              ),
              todayDecoration: BoxDecoration(
                color: Colors.blue.withOpacity(0.5),
                shape: BoxShape.circle,
              ),
              markerDecoration: const BoxDecoration(
                color: Colors.deepOrange,
                shape: BoxShape.circle,
              ),
            ),
            calendarBuilders: CalendarBuilders(
              markerBuilder: (context, date, events) {
                if (events.isNotEmpty) {
                  return Positioned(
                    right: 1,
                    bottom: 1,
                    child: _buildEventsMarker(events.length),
                  );
                }
                return null;
              },
            ),
          ),
          const SizedBox(height: 8.0),
          Expanded(
            child: ValueListenableBuilder<List<Event>>(
              valueListenable: _selectedEvents,
              builder: (context, value, _) {
                return ListView.builder(
                  itemCount: value.length,
                  itemBuilder: (context, index) {
                    return Container(
                      margin: const EdgeInsets.symmetric(
                        horizontal: 12.0,
                        vertical: 4.0,
                      ),
                      decoration: BoxDecoration(
                        border: Border.all(color: Colors.blueAccent),
                        borderRadius: BorderRadius.circular(12.0),
                      ),
                      child: ListTile(
                        onTap: () => print('Tapped event: ${value[index]}'),
                        title: Text(value[index].title),
                      ),
                    );
                  },
                );
              },
            ),
          ),
        ],
      ),
    );
  }

  Widget _buildEventsMarker(int count) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 300),
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.deepOrange[300],
      ),
      width: 16.0,
      height: 16.0,
      child: Center(
        child: Text(
          '$count',
          style: const TextStyle().copyWith(
            color: Colors.white,
            fontSize: 12.0,
          ),
        ),
      ),
    );
  }
}

To run this example, replace the content of your main.dart file with a simple MaterialApp that uses CalendarPage:


import 'package:flutter/material.dart';
import 'package:your_app_name/calendar_page.dart'; // Adjust import path

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Calendar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const CalendarPage(),
    );
  }
}

Remember to create a new file named calendar_page.dart (or similar) for the CalendarPage widget and adjust the import path accordingly.

Conclusion

Creating an interactive calendar widget in Flutter is straightforward with the help of the table_calendar package. This powerful library offers excellent flexibility for both functionality and appearance, allowing developers to build calendars that seamlessly integrate into any application. By mastering date selection, event handling, and customization, you can provide users with a rich and engaging way to manage their schedules and interact with time-based data.

Related Articles

Dec 19, 2025

Building a Widget List with Sticky

Building a Widget List with Sticky Header in Flutter Creating dynamic and engaging user interfaces is crucial for modern applications. One common UI pattern th

Dec 19, 2025

Mastering Transform Scale & Rotate Animations in Flutter

Mastering Transform Scale & Rotate Animations in Flutter Flutter's powerful animation framework allows developers to create visually stunning and highly intera

Dec 19, 2025

Building a Countdown Timer Widget in Flutter

Building a Countdown Timer Widget in Flutter Countdown timers are a fundamental component in many modern applications, ranging from e-commerce platforms indica