linear_date_selector π
A lightweight, customizable horizontal/vertical date selector widget for Flutter.
linear_date_selector provides a simple, robust UI for displaying a linear sequence of dates (e.g., today + next N days) with built-in selection, disabled-date support, animations, and a fully custom tile builderβsimilar to ListView.builder.
Screenshots
![]() |
![]() |
![]() |
|---|
| Default (Horizontal) | Default (Vertical) | Custom Tiles (Horizontal) |
β¨ Features
- Auto-generated date list starting from a provided
todaysDateTime - Horizontal or vertical scrolling
- Default, polished tile UI out of the box
- Fully customizable tiles with
LinearDateSelector.builder(...) - Disable specific dates (non-selectable)
- Optional icon with flexible alignment:
top,bottom,left,right - Tap scale (click) animation
- Optional background and text color animations
- Customizable animation durations
- Simple API and small footprint β no heavy calendar widgets
π Getting started
Add to your pubspec.yaml after publishing:
dependencies:
linear_date_selector: ^0.0.1
Then run:
flutter pub get
Usage
Important: this package's constructors use startDateTime (item index 0 == startDateTime).
Default tiles (quick start)
import 'package:flutter/material.dart';
import 'package:linear_date_selector/linear_date_selector.dart';
// inside a widget build
LinearDateSelector(
startDateTime: DateTime.now(),
itemCount: 7,
onDateTimeSelected: (selected) {
print('selected: $selected');
},
// optional: an icon shown inside each tile
icon: Icon(Icons.event),
iconAlignment: LinearDateSelectorIconAlignment.bottom,
// optional sizing
itemWidth: 72,
itemHeight: 80,
);
π§ͺ Custom builder (full control)
Use .builder to get (context, date, isSelected, isDisabled, index, style) and return any widget:
LinearDateSelector.builder(
listPadding: const EdgeInsets.all(8),
startDateTime: DateTime.now(),
itemCount: 10,
itemHeight: 120,
axis: Axis.horizontal,
disabledDateTimes: [
DateTime.now().add(const Duration(days: 4)),
],
onDateTimeSelected: (d) => print('selected $d'),
itemBuilder: (
context,
date,
isSelected,
isDisabled,
index,
_,
) {
return AnimatedContainer(
duration: const Duration(milliseconds: 200),
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
decoration: BoxDecoration(
color: isDisabled
? Colors.grey.shade200
: (isSelected ? const Color(0xFF1e1405) : const Color(0xFFf8e9d7)),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: isDisabled
? Colors.grey
: (isSelected ? const Color(0xFF1e1405) : const Color(0xFFf8e9d7)),
),
),
child: Column(
children: [
Center(
child: Text.rich(
TextSpan(
children: [
TextSpan(
text: '${DateFormat('dd').format(date)}\n',
style: const TextStyle(fontSize: 28),
),
TextSpan(
text: DateFormat('MMM\nyyyy').format(date),
style: const TextStyle(fontSize: 14),
),
],
),
textAlign: TextAlign.center,
style: TextStyle(
color: isDisabled
? Colors.grey
: (isSelected
? const Color(0xFFf8e9d7)
: const Color(0xFF1e1405)),
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
),
),
),
],
),
);
},
);
Note: The
indexparameter corresponds to the tile's position (0 istodaysDateTime). Use it for special styling (e.g., first/last tile) or animations.
π Date-range (default tiles)
LinearDateSelector.dateRange(
startDateTime: DateTime(2025, 1, 1),
endDateTime: DateTime(2025, 1, 7), // inclusive β 1,2,3,4,5,6,7
onDateTimeSelected: (selected) {
print('selected: $selected');
},
// optional extras
icon: const Icon(Icons.event),
iconAlignment: LinearDateSelectorIconAlignment.top,
itemWidth: 70,
itemHeight: 85,
enableColorAnimation: true,
listPadding: const EdgeInsets.symmetric(horizontal: 12),
);
π¨ π§ͺ Date-range + custom builder (full control)
LinearDateSelector.dateRangeBuilder(
startDateTime: DateTime(2025, 2, 10),
endDateTime: DateTime(2025, 2, 15), // inclusive (6 days)
disabledDateTimes: [
DateTime(2025, 2, 12), // middle day disabled
],
onDateTimeSelected: (selected) {
print('picked: $selected');
},
listPadding: const EdgeInsets.all(10),
axis: Axis.horizontal,
itemWidth: 90,
itemHeight: 120,
itemBuilder: (
context,
date,
isSelected,
isDisabled,
index,
style,
) {
final day = DateFormat('dd').format(date);
final month = DateFormat('MMM').format(date);
return AnimatedContainer(
duration: const Duration(milliseconds: 250),
margin: const EdgeInsets.symmetric(horizontal: 6),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isDisabled
? Colors.grey.shade300
: (isSelected ? Colors.blue : Colors.white),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: isSelected ? Colors.blue : Colors.grey.shade400,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
day,
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: isDisabled ? Colors.grey : Colors.black,
),
),
Text(
month,
style: TextStyle(
fontSize: 16,
color: isDisabled ? Colors.grey : Colors.black87,
),
),
],
),
);
},
);
π When to use:
-
You want different UI for weekdays vs weekends
-
You want animations, badges, chips, colors, shadows
-
UI designers give you custom Figma layouts
-
You need total control but want range logic for free
Note: .
dateRangeand .dateRangeBuilderignore hours and minutes in the given DateTimes. Only the calendar day is used:
DateTime(2025, 1, 1, 23:00)
β treated as 2025-01-01
API
LinearDateSelector
Constructors:
LinearDateSelector(...)β default tiles and optional animations.LinearDateSelector.builder(...)β accept a customitemBuilderfunction.LinearDateSelector.dateRange(...)β inclusive date-range selector (default tile UI). Time-of-day is ignored (dates are normalized to year/month/day). -LinearDateSelector.dateRangeBuilder(...)- Same asdateRange, but allows a fully customitemBuilderfor each date tile. Range generation is handled for you; you only render the tiles.
Important properties:
-
startDateTimeβDateTimewhere the list starts (index 0). -
endDateTimeβDateTimewhere the list ends (optional). -
onDateTimeSelectedβFunction(DateTime)callback when a date is chosen. -
disabledDateTimesβList<DateTime>of dates that should be rendered disabled. -
itemCountβ number of tiles to show (must be > 0). -
axisβAxis.horizontal(default) orAxis.vertical. -
itemWidth,itemHeightβ optional size of each tile. -
icon,iconAlignmentβ optional icon and position. -
listPaddingβ padding around the scrollable list. -
enableClickAnimationβ whether tap-scale animation is enabled (defaulttrue). -
scaleAnimationDurationβ duration for the tap-scale animation. -
enableColorAnimationβ enables background/text color animation for default tiles. -
backgroundColorChangeDuration,textColorChangeDurationβ durations for color animations (used by the default tile implementation). -
physics,controllerβ optionalScrollPhysicsandScrollControllerfor the list. -
itemBuilderβWidget Function(BuildContext context, DateTime date, bool isSelected, bool isDisabled, int index, DateSelectorStyle style)?for fully-custom tile rendering.
DateSelectorStyle
A simple style holder to customize tile colors, border colors and icon padding. Pass it into the widget's style: parameter to tweak the default tile appearance.
class DateSelectorStyle {
final Color tileBackgroundColor;
final Color selectedTileBackgroundColor;
final Color borderColor;
final Color selectedBorderColor;
final Color selectedTextColor;
final Color disabledTileBackgroundColor;
final Color disabledTextColor;
final Color disabledBorderColor;
final EdgeInsets? iconPadding;
const DateSelectorStyle({...});
}
π§ͺExample: disabling dates
final today = DateTime.now();
final disabled = [
DateTime(today.year, today.month, today.day + 2),
];
LinearDateSelector(
todaysDateTime: today,
itemCount: 7,
disabledDateTimes: disabled,
onDateTimeSelected: (date) {
// will not be fired for disabled dates
},
);
π§ Accessibility & Tips
-
Consider wrapping your custom builder tile in
Semanticsto exposeselected/disabledto screen readers. -
The default tile uses
AnimatedContainerandAnimatedDefaultTextStyleto animate color changes whenenableColorAnimationis true. -
For timezone-sensitive apps, normalize dates to avoid surprising disabled/selected behavior.
-
If you want the selector to start with a tile pre-selected, coordinate the initial selection via the parent: call
onDateTimeSelectedafter widget build or extend the widget to accept aninitialSelectedDateparameter.
Behavior & Notes
-
itemCountmust be > 0 β an assert will fail in debug if it's not. -
disabledDateTimesare matched by calendar day (year-month-day). Time components are ignored, soDateTime(2025, 12, 1, 0, 0)andDateTime(2025, 12, 1, 13, 0)refer to the same disabled date. -
Selection state (
selectedIndex) is stored locally inside the widget. UseonDateTimeSelectedto communicate selection to parent widgets and keep external state in sync if needed. -
The widget precomputes the list of consecutive dates for performance. If you update
startDateTime,itemCount, ordisabledDateTimes, the selector will recompute its internal lists (so changes are reflected). -
If you use different
DateTimetimezones or pass dates with time components, you may want to normalize them (e.g., to local midnight or UTC) before passing to disabledDateTimes for predictable comparisons.
Contributing
Contributions, bug reports, and feature requests are welcome. Open an issue or submit a PR.
When contributing, please:
- Follow the repository's
analysis_options.dartand formatting conventions. - Add tests for behavior-critical changes (selection, disabled logic, and builder behavior).
Changelog
- 0.0.1 β Initial release: default tiles, builder API, disabled-date support.
- Recomputes internal lists when important inputs change.
π License
This package is distributed under the MIT LICENSE. See LICENSE for more information.
Author
Created by the package author. Feel free to open issues or PRs for improvements.


