Building an Event Calendar Widget with Highlighted Dates in Flutter

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

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en