TableCalendar: a month grid for Flutter that refuses to own your state
Highly customizable, feature-packed calendar widget for Flutter
At a glance
- What is it?
- A Flutter calendar widget that renders a configurable month grid and hands day selection, event loading and page changes back to your own State object. Good for apps that need a booking or availability grid, and awkward for anyone who wants the widget to remember the focused month on its own.
- Who is it for?
- Adopt TableCalendar if your Flutter screen needs a month grid with events, ranges or custom day cells, and if you are willing to hold focusedDay and the selection in your own State. Do not reach for it when you want a calendar that manages its own scroll position, or when you need a resource booking grid with hour level slots, because the formats it ships are month, two weeks and week.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 22 days ago.
- What is it written in?
- Mainly Dart, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The widget draws a grid and delegates every decision back to you
TableCalendar is a Dart package published to pub.dev under the name table_calendar, released under Apache-2.0, with its source in lib/ and a full example application in example/ covering android, ios and web targets. The last push was on 2026-10-01 and the repository publishes no GitHub releases, so version tracking happens through the pub constraint rather than through release tags.
What the package actually provides is a calendar surface plus a set of callbacks. Styling, day cell rendering, day-of-week labels and event markers are all replaceable, but the values driving them come from you. That split is the defining design choice. A calendar that owns its own state is easier to drop in and harder to drive from a server backed booking screen; this one does the opposite.
firstDay, lastDay and focusedDay are the three properties you cannot skip
The constructor has no defaults for the three properties that define what the calendar can show and where it starts. firstDay is the first available day and users cannot reach days before it. lastDay is the last available day, with the same restriction after it. focusedDay is the currently targeted day and it is what decides which month is visible.
The minimal widget therefore looks like this:
TableCalendar(
firstDay: DateTime.utc(2010, 10, 16),
lastDay: DateTime.utc(2030, 3, 14),
focusedDay: DateTime.now(),
);Note that firstDay and lastDay are the horizontal swipe boundaries, so the range you pick is simultaneously the range your users can scroll through. A scheduler app that wants to show a year of history has to open firstDay far enough back, because the widget offers no way to load months lazily outside the declared window.
Adding the dependency and making taps do something
Installation is a pubspec entry, and the README pins the constraint to the 3.3.0 line:
dependencies:
table_calendar: ^3.3.0Out of the box the widget is not interactive in the way most people expect. You can swipe horizontally to change the visible month, and that is all. Making a tap select a day requires two pieces: a predicate that tells the widget which day is currently selected, and a callback that receives the tap.
selectedDayPredicate: (day) {
return isSameDay(_selectedDay, day);
},
onDaySelected: (selectedDay, focusedDay) {
setState(() {
_selectedDay = selectedDay;
_focusedDay = focusedDay; // update `_focusedDay` here as well
});
},Both are required together. The predicate alone marks a day without telling your app about it, and the callback alone stores a selection the widget will not highlight.
onPageChanged is what stops your calendar snapping back on hot reload
Passing a static focusedDay is the mistake almost every new user makes. Because the value never changes, every rebuild uses that same day. The README shows how to see it: set focusedDay to DateTime.now(), swipe to the next month, then trigger a hot reload, and the calendar resets to its initial state. In a shipped app the same mechanism shows up as the view jumping back after a rotation or any unrelated setState.
The fix is a third callback that stores whatever day the widget reports as focused:
onPageChanged: (focusedDay) {
_focusedDay = focusedDay;
},One subtlety the documentation calls out: you should not wrap this in setState(). The value is stored so a later rebuild picks it up; calling setState inside the page change callback causes a redundant rebuild during the swipe animation. The same reasoning applies inside onDaySelected, where you do need setState because your selection also drives other widgets.
eventLoader turns the grid into an availability or booking view
Events are supplied through a single loader function that receives a DateTime and returns a list. The implementation is entirely yours, so a Map keyed by day is the common shape:
eventLoader: (day) {
return _getEventsForDay(day);
},with a lookup beside it:
List<Event> _getEventsForDay(DateTime day) {
return events[day] ?? [];
}The DateTime equality trap is the part worth planning around. A DateTime carries a time part that has no meaning for a calendar, so a plain Map lookup misses for anything that was not constructed at exactly midnight. The README recommends a LinkedHashMap with custom comparison functions:
final events = LinkedHashMap(
equals: isSameDay,
hashCode: getHashCode,
)..addAll(eventSource);The same is true for selection state, which is why selectedDayPredicate and onDaySelected both go through isSameDay rather than comparing DateTime objects directly.
Recurring events are a loader pattern, not a separate feature
Because the loader is a function rather than a data structure, repeating patterns fall out of it without any dedicated API. Every Monday gets an event by testing the weekday:
eventLoader: (day) {
if (day.weekday == DateTime.monday) {
return [Event('Cyclic event')];
}
return [];
},This is a genuine advantage when your schedule comes from rules rather than rows, since no indexing step is needed and no cache can go stale. The cost is that every visible day triggers a call, so a loader that hits a database or a network endpoint needs its own memoization. The documentation does not describe any caching inside the widget, so treat the loader as something you call often.
CalendarBuilders overrides one cell at a time, and null means keep the default
Styling and structure are split into two mechanisms. CalendarFormat controls the shape of the grid, and the feature list names month, two weeks and week, switched at runtime through calendarFormat with onFormatChanged writing the new value back into your state. Vertical autosizing is a separate decision: the calendar either fits its content or fills the viewport.
Within a given format, CalendarBuilders lets you replace individual pieces. Each builder can return null, which falls back to the default rendering for that slot. The documented example overrides only the Sunday label and leaves the rest of the day-of-week row alone:
calendarBuilders: CalendarBuilders(
dowBuilder: (context, day) {
if (day.weekday == DateTime.sunday) {
final text = DateFormat.E().format(day);The block is truncated in the README itself, which is a fair signal of how much of this API you are expected to read from the pub.dev documentation rather than the project page.
That selective design is what makes range selection, multiple selection and custom event chips possible without forking the widget. The trade-off is discoverability: the API surface is large, and the README pushes readers to the pub.dev API documentation and the example pages for the full picture.
Where this package stops and something else takes over
The formats are the boundary. Month, two weeks and week cover scheduling and planning screens, with locale support for the day-of-week labels and horizontal swipe boundaries clamped to firstDay and lastDay. What is not here is any notion of time slots inside a day. A barber shop booking screen or a meeting room scheduler that needs hourly rows per day needs a different component, because TableCalendar's cell is a day and nothing below that.
The second boundary is state ownership. Every interaction the README documents ends in a callback that writes to your State, which is the right trade for an app whose calendar state is derived from a server. It is the wrong trade for a screen where the calendar is decorative and you would rather it remember its own position. In that case a self contained widget with internal state would remove the three callbacks you now have to wire by hand.
Editorial conclusion
Adopt TableCalendar if your Flutter screen needs a month grid with events, ranges or custom day cells, and if you are willing to hold focusedDay and the selection in your own State. Do not reach for it when you want a calendar that manages its own scroll position, or when you need a resource booking grid with hour level slots, because the formats it ships are month, two weeks and week. Before you commit, run the pubspec constraint table_calendar: ^3.3.0 against your current Flutter SDK, and confirm that your onPageChanged callback updates the stored focusedDay.
Frequently asked questions
How do I add TableCalendar to a Flutter project?
Add the dependency line table_calendar: ^3.3.0 under dependencies in pubspec.yaml, then import the package and place a TableCalendar in your widget tree with firstDay, lastDay and focusedDay set.
Why does my TableCalendar jump back to the original month after a rebuild?
A static focusedDay is reapplied on every rebuild. The README recommends storing the day passed to onPageChanged so the widget uses the updated value, and notes that you should not call setState() inside that callback.
How do I show events on specific days in TableCalendar?
Provide an eventLoader that takes a DateTime and returns a list of events for that day. The implementation is yours, and the README suggests a LinkedHashMap with equals: isSameDay so DateTime values compare by date only.
Does TableCalendar support selecting a range of days or multiple days?
The feature list states range selection support and multiple selection support. The selection itself is held in your own state and reported through callbacks such as onDaySelected.
Which calendar formats does TableCalendar offer?
Multiple calendar formats are listed: month, two weeks and week. You switch between them at runtime with calendarFormat and onFormatChanged.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/aleksanderwozniak-table-calendar)Was this useful?