image

08 Feb 2026

9K

35K

Building an Event Calendar Widget with Highlighted Dates in Flutter

Event calendars are a fundamental component in many applications, from productivity tools to social platforms. They provide users with a clear visual representation of upcoming events, deadlines, and important dates. In Flutter, building a sophisticated event calendar, complete with date highlighting and event display, can be achieved efficiently using powerful third-party libraries. This article will guide you through the process of creating such a widget, focusing on a professional and maintainable approach.

1. Choosing the Right Library: table_calendar

While one could build a calendar from scratch, leveraging existing, well-maintained libraries is often the most productive path. For Flutter event calendars, table_calendar stands out as a robust and highly customizable option. It offers a flexible API for displaying dates, handling selections, and integrating event data.

2. Project Setup

First, add table_calendar to your Flutter project's pubspec.yaml file. You might also want to add intl for date formatting if not already present, though `table_calendar` handles some formatting internally.


dependencies:
  flutter:
    sdk: flutter
  table_calendar: ^3.0.9 # Use the latest stable version
  intl: ^0.18.1 # For date formatting (optional, but good practice)

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

3. Basic Calendar Implementation

A basic TableCalendar can be quickly set up within a StatefulWidget. This initial setup will display a calendar with no special highlighting or event handling.


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

class EventCalendarScreen extends StatefulWidget {
  const EventCalendarScreen({Key? key}) : super(key: key);

  @override
  _EventCalendarScreenState createState() => _EventCalendarScreenState();
}

class _EventCalendarScreenState extends State {
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Event Calendar')),
      body: TableCalendar(
        firstDay: DateTime.utc(2010, 10, 16),
        lastDay: DateTime.utc(2030, 3, 14),
        focusedDay: _focusedDay,
        selectedDayPredicate: (day) {
          return isSameDay(_selectedDay, day);
        },
        onDaySelected: (selectedDay, focusedDay) {
          setState(() {
            _selectedDay = selectedDay;
            _focusedDay = focusedDay; // update `_focusedDay` to make sure no day is marked as focused
          });
        },
        calendarFormat: CalendarFormat.month,
        headerStyle: const HeaderStyle(
          formatButtonVisible: false,
          titleCentered: true,
        ),
        calendarStyle: const CalendarStyle(
          outsideDaysVisible: false,
        ),
      ),
    );
  }
}

In this snippet:

  • firstDay and lastDay define the scrollable date range.
  • focusedDay determines which month is currently displayed.
  • selectedDayPredicate and onDaySelected are used to manage user selections.

4. Structuring Event Data

To highlight dates with events, we need a way to store our event data. A simple class for an event and a Map where keys are DateTime objects and values are lists of events for that day is a common and effective pattern.


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

  @override
  String toString() => title;
}

// Example event data structure
// A LinkedHashMap is often used to maintain insertion order,
// but a regular Map> works fine too.
final Map> kEvents = {
  DateTime.utc(2023, 11, 15): [const Event('Project Review')],
  DateTime.utc(2023, 11, 16): [const Event('Team Meeting'), const Event('Client Call')],
  DateTime.utc(2023, 11, 20): [const Event('Conference Day 1')],
  DateTime.utc(2023, 11, 21): [const Event('Conference Day 2'), const Event('Networking Event')],
  DateTime.utc(2023, 12, 1): [const Event('Holiday Party')],
};

// Helper function to normalize DateTime objects to just year, month, day.
int getHashCode(DateTime key) {
  return key.day * 1000000 + key.month * 10000 + key.year;
}

/// Returns a list of [DateTime] objects that are the days in a given week.
List daysInRange(DateTime first, DateTime last) {
  final dayCount = last.difference(first).inDays + 1;
  return List.generate(
    dayCount,
    (index) => DateTime.utc(first.year, first.month, first.day + index),
  );
}

We use DateTime.utc for consistency, ensuring that date comparisons are independent of local time zones.

5. Highlighting Dates with Events

table_calendar provides an eventLoader callback that takes a DateTime object and returns a List of events for that day. This list's length is used to display "event dots" under the date.

First, create a helper function to retrieve events for a given day:


List _getEventsForDay(DateTime day) {
  // Use a simplified key for the map (year, month, day only)
  // Ensure kEvents also uses normalized DateTime objects as keys
  return kEvents[DateTime.utc(day.year, day.month, day.day)] ?? [];
}

Now, integrate this into your TableCalendar:


// Inside your TableCalendar widget's build method
TableCalendar(
  // ... other properties ...
  eventLoader: _getEventsForDay, // Add this line
  calendarBuilders: CalendarBuilders(
    markerBuilder: (context, day, events) {
      if (events.isNotEmpty) {
        return Positioned(
          right: 1,
          bottom: 1,
          child: _buildEventsMarker(events),
        );
      }
      return null;
    },
  ),
  // ...
);

// Helper method for the markerBuilder
Widget _buildEventsMarker(List events) {
  return AnimatedContainer(
    duration: const Duration(milliseconds: 300),
    decoration: BoxDecoration(
      shape: BoxShape.circle,
      color: Colors.blue[400],
    ),
    width: 16.0,
    height: 16.0,
    child: Center(
      child: Text(
        '${events.length}',
        style: const TextStyle().copyWith(
          color: Colors.white,
          fontSize: 12.0,
        ),
      ),
    ),
  );
}

The markerBuilder gives you full control over how event markers are displayed. Here, we're showing a simple blue circle with the count of events.

6. Displaying Events for the Selected Date

Typically, when a user selects a date, they expect to see the details of events scheduled for that day. We can display these events in a list below the calendar.


// Add this list to your _EventCalendarScreenState
List _selectedEvents = [];

// Modify _onDaySelected to update _selectedEvents
@override
void initState() {
  super.initState();
  // Initialize _selectedDay and _selectedEvents
  _selectedDay = _focusedDay;
  _selectedEvents = _getEventsForDay(_selectedDay!);
}

// Update the onDaySelected callback
onDaySelected: (selectedDay, focusedDay) {
  setState(() {
    _selectedDay = selectedDay;
    _focusedDay = focusedDay;
    _selectedEvents = _getEventsForDay(selectedDay); // Update events for the selected day
  });
},

// Below the TableCalendar, add an Expanded ListView to show events
// ...
body: Column(
  children: [
    TableCalendar(
      // ... calendar properties ...
    ),
    const SizedBox(height: 8.0),
    Expanded(
      child: ListView.builder(
        itemCount: _selectedEvents.length,
        itemBuilder: (context, index) {
          final event = _selectedEvents[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('Event tapped: ${event.title}'),
              title: Text(event.title),
            ),
          );
        },
      ),
    ),
  ],
),

7. Putting It All Together (Full Example)

Here's the complete code for the EventCalendarScreen demonstrating all the concepts discussed.


import 'package:flutter/material.dart';
import 'package:table_calendar/table_calendar.dart';
import 'package:intl/intl.dart'; // For date formatting if needed, though not strictly used in this example.

class EventCalendarScreen extends StatefulWidget {
  const EventCalendarScreen({Key? key}) : super(key: key);

  @override
  _EventCalendarScreenState createState() => _EventCalendarScreenState();
}

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

  @override
  String toString() => title;
}

// Example event data structure
// Ensure all DateTime keys are normalized to UTC with hour/minute/second set to 0.
final Map> kEvents = {
  DateTime.utc(2023, 11, 15): [const Event('Project Review')],
  DateTime.utc(2023, 11, 16): [const Event('Team Meeting'), const Event('Client Call')],
  DateTime.utc(2023, 11, 20): [const Event('Conference Day 1')],
  DateTime.utc(2023, 11, 21): [const Event('Conference Day 2'), const Event('Networking Event')],
  DateTime.utc(2023, 12, 1): [const Event('Holiday Party')],
  DateTime.utc(2023, 12, 10): [const Event('Annual Checkup')],
  DateTime.utc(2024, 1, 5): [const Event('New Year Planning')],
};

class _EventCalendarScreenState extends State {
  DateTime _focusedDay = DateTime.now();
  DateTime? _selectedDay;
  List _selectedEvents = [];

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

  List _getEventsForDay(DateTime day) {
    // Normalize the day to match the keys in kEvents (year, month, day only).
    return kEvents[DateTime.utc(day.year, day.month, day.day)] ?? [];
  }

  void _onDaySelected(DateTime selectedDay, DateTime focusedDay) {
    if (!isSameDay(_selectedDay, selectedDay)) {
      setState(() {
        _selectedDay = selectedDay;
        _focusedDay = focusedDay; // update `_focusedDay` to make sure no day is marked as focused
        _selectedEvents = _getEventsForDay(selectedDay);
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Event Calendar')),
      body: Column(
        children: [
          TableCalendar(
            firstDay: DateTime.utc(2010, 10, 16),
            lastDay: DateTime.utc(2030, 3, 14),
            focusedDay: _focusedDay,
            selectedDayPredicate: (day) {
              return isSameDay(_selectedDay, day);
            },
            onDaySelected: _onDaySelected,
            eventLoader: _getEventsForDay,
            calendarFormat: CalendarFormat.month,
            headerStyle: const HeaderStyle(
              formatButtonVisible: false,
              titleCentered: true,
            ),
            calendarStyle: CalendarStyle(
              outsideDaysVisible: false,
              todayDecoration: BoxDecoration(
                color: Colors.blue.withOpacity(0.2),
                shape: BoxShape.circle,
              ),
              selectedDecoration: const BoxDecoration(
                color: Colors.blue,
                shape: BoxShape.circle,
              ),
              markerDecoration: BoxDecoration(
                color: Colors.red[400],
                shape: BoxShape.circle,
              ),
            ),
            calendarBuilders: CalendarBuilders(
              markerBuilder: (context, day, events) {
                if (events.isNotEmpty) {
                  return Positioned(
                    right: 1,
                    bottom: 1,
                    child: _buildEventsMarker(events),
                  );
                }
                return null;
              },
            ),
          ),
          const SizedBox(height: 8.0),
          Expanded(
            child: _selectedEvents.isEmpty
                ? const Center(child: Text('No events for this day.'))
                : ListView.builder(
                    itemCount: _selectedEvents.length,
                    itemBuilder: (context, index) {
                      final event = _selectedEvents[index];
                      return Container(
                        margin: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 4.0),
                        decoration: BoxDecoration(
                          border: Border.all(color: Colors.grey.shade300),
                          borderRadius: BorderRadius.circular(12.0),
                        ),
                        child: ListTile(
                          onTap: () {
                            // Handle event tap, e.g., navigate to event details screen
                            print('Event tapped: ${event.title}');
                          },
                          title: Text(event.title),
                          leading: const Icon(Icons.event),
                          trailing: const Icon(Icons.arrow_forward_ios, size: 16),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }

  Widget _buildEventsMarker(List events) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 300),
      decoration: BoxDecoration(
        shape: BoxShape.circle,
        color: Colors.red[400], // Marker color for days with events
      ),
      width: 16.0,
      height: 16.0,
      child: Center(
        child: Text(
          '${events.length}',
          style: const TextStyle().copyWith(
            color: Colors.white,
            fontSize: 10.0,
          ),
        ),
      ),
    );
  }
}

Conclusion

Building an interactive event calendar with highlighted dates in Flutter is significantly streamlined by using the table_calendar package. By properly structuring your event data, implementing the eventLoader, and handling date selections, you can create a feature-rich calendar that enhances user experience. The customizability of table_calendar allows you to tailor its appearance and behavior to perfectly match your application's design language and functional requirements.

Related Articles

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is