M
Marai.UI
v1.0.0-alpha.7

Date Picker

A date, date+time, time, or date-range picker with a two-month calendar view, month/year dropdowns, full Tailwind class control, and no built-in visual styles.

Overview

MDatePicker supports single-value modes (Date, DateTime, Time) and a date-range mode (IsDateRange="true"). All bind to DateTime?; range mode adds an EndValue binding for the end date. The calendar header provides month and year <select> dropdowns for quick navigation alongside prev/next buttons. The panel is fully unstyled — every surface accepts a dedicated class parameter. It opens on trigger click and closes on Escape or after selection. Dates outside Min/Max are disabled.

Usage

Add the namespace import and include the stylesheet reference if not already done.

razor
@using Marai.UI.Components.MDatePicker

Date Only

The default mode. Opens a two-month calendar and commits the selected date at midnight. The panel closes automatically when a date is chosen.

Preview

Selected: none

razor
<div class="flex flex-col gap-2">
    <MLabel For="date-only">Pick a date</MLabel>
    <MDatePicker id="date-only"
                 @bind-Value="_date"
                 Placeholder="Select a date"
                 Class="relative inline-block"
                 TriggerClass="w-64 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-left text-slate-700 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-200 bg-white p-4 shadow-lg"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-400 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-700 hover:bg-sky-50 cursor-pointer"
                 SelectedDayClass="!bg-sky-600 !text-white hover:!bg-sky-700"
                 TodayDayClass="font-bold text-sky-600"
                 DisabledDayClass="cursor-not-allowed opacity-30 hover:bg-transparent" />
    <p class="text-sm text-slate-500">Selected: @(_date.HasValue ? _date.Value.ToString("MMMM d, yyyy") : "none")</p>
</div>

@code {
    private DateTime? _date;
}

Examples

Date + Time

Set Mode="MDatePickerMode.DateTime" to show the calendar alongside a native input type="time". Selecting a new date preserves the current time component. Use TimeInputClass to style the time field.

Preview

Selected: none

razor
<div class="flex flex-col gap-2">
    <MLabel For="date-time">Appointment</MLabel>
    <MDatePicker id="date-time"
                 Mode="MDatePickerMode.DateTime"
                 @bind-Value="_dateTime"
                 Placeholder="Select date &amp; time"
                 Class="relative inline-block"
                 TriggerClass="w-64 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-left text-slate-700 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-200 bg-white p-4 shadow-lg"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-400 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-700 hover:bg-sky-50 cursor-pointer"
                 SelectedDayClass="!bg-sky-600 !text-white hover:!bg-sky-700"
                 TodayDayClass="font-bold text-sky-600"
                 DisabledDayClass="cursor-not-allowed opacity-30 hover:bg-transparent"
                 TimeInputClass="mt-3 w-full rounded-md border border-slate-200 bg-slate-50 px-3 py-1.5 text-sm text-slate-700 focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <p class="text-sm text-slate-500">Selected: @(_dateTime.HasValue ? _dateTime.Value.ToString("MMM d, yyyy h:mm tt") : "none")</p>
</div>

@code {
    private DateTime? _dateTime;
}

Time Only

Mode="MDatePickerMode.Time" renders only a native input type="time". The bound value stores the chosen time on DateTime.Today. No calendar panel is shown. Use TimeInputClass for all visual styling.

Preview

Selected: none

razor
<div class="flex flex-col gap-2">
    <MLabel For="time-only">Meeting time</MLabel>
    <MDatePicker id="time-only"
                 Mode="MDatePickerMode.Time"
                 @bind-Value="_time"
                 TimeInputClass="w-40 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-slate-900 focus:outline-none focus:ring-2 focus:ring-sky-500" />
    <p class="text-sm text-slate-500">Selected: @(_time.HasValue ? _time.Value.ToString("h:mm tt") : "none")</p>
</div>

@code {
    private DateTime? _time;
}

Date Range

Set IsDateRange="true" and bind both @bind-Value (start) and @bind-EndValue (end). The first click sets the start date; the second click sets the end date and closes the panel. Clicking before the current start resets the selection. While selecting the end date, hovering over days previews the range using InRangeClass. Use RangeStartClass and RangeEndClass to differentiate the endpoints visually.

Preview

Check-in:  →  Check-out:

razor
<div class="flex flex-col gap-2">
    <MLabel For="range">Book a stay</MLabel>
    <MDatePicker id="range"
                 IsDateRange="true"
                 DisplayMonths="2"
                 @bind-Value="_start"
                 @bind-EndValue="_end"
                 Placeholder="Select check-in – check-out"
                 Class="relative inline-block"
                 TriggerClass="w-72 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-left text-slate-700 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-200 bg-white p-4 shadow-lg"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-400 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-700 hover:bg-sky-50 cursor-pointer"
                 SelectedDayClass="!bg-sky-600 !text-white hover:!bg-sky-700"
                 TodayDayClass="font-bold text-sky-600"
                 DisabledDayClass="cursor-not-allowed opacity-30 hover:bg-transparent"
                 RangeStartClass="!rounded-r-none"
                 RangeEndClass="!rounded-l-none"
                 InRangeClass="!rounded-none !bg-sky-100 !text-sky-800 hover:!bg-sky-200" />

    <p class="text-sm text-slate-500">
        Check-in: <strong>@(_start.HasValue ? _start.Value.ToString("MMM d, yyyy") : "—")</strong>
        &nbsp;→&nbsp;
        Check-out: <strong>@(_end.HasValue ? _end.Value.ToString("MMM d, yyyy") : "—")</strong>
    </p>
</div>

@code {
    private DateTime? _start;
    private DateTime? _end;
}

Min / Max Constraints

Set Min and/or Max to restrict the selectable range. Days outside the range render with the native disabled attribute. Apply visual disabled styling (strikethrough, reduced opacity) via DisabledDayClass.

Preview

Range: Jun 1 – Jun 30, 2026

Selected: none

razor
<div class="flex flex-col gap-2">
    <MLabel For="min-max">Book within this month</MLabel>
    <MDatePicker id="min-max"
                 @bind-Value="_date"
                 Min="_min"
                 Max="_max"
                 Placeholder="Select a date"
                 Class="relative inline-block"
                 TriggerClass="w-64 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-left text-slate-700 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-200 bg-white p-4 shadow-lg"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-400 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-700 hover:bg-sky-50 cursor-pointer"
                 SelectedDayClass="!bg-sky-600 !text-white hover:!bg-sky-700"
                 TodayDayClass="font-bold text-sky-600"
                 DisabledDayClass="cursor-not-allowed opacity-30 line-through hover:bg-transparent" />
    <p class="text-sm text-slate-500">
        Range: @_min.ToString("MMM d") – @_max.ToString("MMM d, yyyy")
    </p>
    <p class="text-sm text-slate-500">Selected: @(_date.HasValue ? _date.Value.ToString("MMMM d, yyyy") : "none")</p>
</div>

@code {
    private DateTime? _date;
    private DateTime _min = new DateTime(DateTime.Today.Year, DateTime.Today.Month, 1);
    private DateTime _max = new DateTime(DateTime.Today.Year, DateTime.Today.Month,
        DateTime.DaysInMonth(DateTime.Today.Year, DateTime.Today.Month));
}

Full Tailwind Customization

Every visual surface — trigger, panel, month headers, nav buttons, day cells — accepts a dedicated class parameter. The example below builds a dark-themed picker entirely through Tailwind utilities.

Preview

Selected: none

razor
<div class="flex flex-col gap-2">
    <MLabel For="custom">Dark themed picker</MLabel>
    <MDatePicker id="custom"
                 @bind-Value="_date"
                 Placeholder="Choose a date"
                 Class="relative inline-block"
                 TriggerClass="w-64 rounded-lg bg-slate-800 px-4 py-2 text-sm text-left text-slate-100 border border-slate-600 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-700 bg-slate-900 p-4 shadow-2xl"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-400 hover:bg-slate-700 hover:text-white text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-200 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-200 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-500 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-300 hover:bg-slate-700 cursor-pointer"
                 SelectedDayClass="!bg-violet-600 !text-white hover:!bg-violet-700"
                 TodayDayClass="font-bold !text-violet-400"
                 DisabledDayClass="cursor-not-allowed opacity-25 hover:bg-transparent" />
    <p class="text-sm text-slate-500">Selected: @(_date.HasValue ? _date.Value.ToString("MMMM d, yyyy") : "none")</p>
</div>

@code {
    private DateTime? _date;
}

Two-Way Binding

Use @bind-Value to keep a DateTime? field in sync. You can read or write the value programmatically — the calendar view will follow the bound value.

Preview

Bound value: null

razor
<div class="flex flex-col gap-3">
    <MLabel For="binding">Start date</MLabel>
    <MDatePicker id="binding"
                 @bind-Value="_date"
                 Placeholder="Pick a start date"
                 Class="relative inline-block"
                 TriggerClass="w-64 rounded-md border border-slate-300 bg-white px-3 py-2 text-sm text-left text-slate-700 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-sky-500"
                 BackdropClass="z-[9]"
                 PanelClass="absolute z-10 mt-1 rounded-xl border border-slate-200 bg-white p-4 shadow-lg"
                 MonthClass="w-56"
                 HeaderClass="flex items-center justify-between gap-1 mb-3"
                 NavButtonClass="flex h-7 w-7 items-center justify-center rounded-md text-slate-500 hover:bg-slate-100 text-lg leading-none"
                 MonthSelectClass="flex-1 bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 YearSelectClass="bg-transparent text-sm font-semibold text-slate-800 focus:outline-none cursor-pointer"
                 DayGridClass="gap-y-1 text-center"
                 DayOfWeekClass="text-xs font-medium text-slate-400 flex items-center justify-center h-8"
                 DayClass="flex h-8 w-full items-center justify-center rounded-md text-sm text-slate-700 hover:bg-sky-50 cursor-pointer"
                 SelectedDayClass="!bg-sky-600 !text-white hover:!bg-sky-700"
                 TodayDayClass="font-bold text-sky-600"
                 DisabledDayClass="cursor-not-allowed opacity-30 hover:bg-transparent" />

    <div class="flex gap-2">
        <button class="rounded-md border border-slate-300 px-3 py-1 text-sm hover:bg-slate-50"
                @onclick="SetToday">
            Set today
        </button>
        <button class="rounded-md border border-slate-300 px-3 py-1 text-sm hover:bg-slate-50"
                @onclick="Clear">
            Clear
        </button>
    </div>

    <p class="text-sm text-slate-500">
        Bound value: <strong>@(_date.HasValue ? _date.Value.ToString("yyyy-MM-dd") : "null")</strong>
    </p>
</div>

@code {
    private DateTime? _date;

    private void SetToday() => _date = DateTime.Today;
    private void Clear() => _date = null;
}

Accessibility

  • The trigger button exposes aria-expanded and aria-haspopup="dialog"
  • Each day button carries aria-label (full date) and aria-pressed (selected state)
  • Navigation buttons have aria-label="Previous month" and aria-label="Next month"
  • Pressing Escape closes the panel
  • Disabled dates use the native disabled attribute — they are skipped by keyboard navigation automatically
  • Always pair the trigger with an MLabel using matching id and For attributes

API Reference

MDatePicker

Parameter Type Default Description
Value DateTime? null The selected date/time. Use with @bind-Value for two-way binding.
ValueChanged EventCallback<DateTime?> Callback invoked when the selection changes. Wired automatically by @bind-Value.
ValueExpression Expression<Func<DateTime?>>? null Expression identifying the bound field for <ValidationMessage>. Set automatically by @bind-Value.
IsDateRange bool false Enables range selection mode. The first click sets Value (start); the second click sets EndValue (end) and closes the panel.
EndValue DateTime? null The range end date. Only used when IsDateRange="true". Use with @bind-EndValue.
EndValueChanged EventCallback<DateTime?> Callback invoked when the end date changes. Wired automatically by @bind-EndValue.
EndValueExpression Expression<Func<DateTime?>>? null Expression for <ValidationMessage> on the end date field.
Mode MDatePickerMode Date Controls what the picker renders: calendar only (Date), calendar + time input (DateTime), or time input only (Time).
DisplayMonths int 2 Number of adjacent months displayed in the calendar panel.
Min DateTime? null Earliest selectable date. Days before this are disabled.
Max DateTime? null Latest selectable date. Days after this are disabled.
Placeholder string? null Text shown in the trigger button when no value is selected.
Disabled bool false Disables the trigger button (or the time input in Time mode).
Class string? null Applied to the root container <div> in Date / DateTime modes.
TriggerClass string? null Applied to the trigger <button>.
PanelClass string? null Applied to the calendar panel container.
MonthClass string? null Applied to each individual month grid container.
HeaderClass string? null Applied to the month header row (title + nav buttons).
NavButtonClass string? null Applied to the previous/next month navigation buttons.
MonthSelectClass string? null Applied to the <select> element for month navigation in the calendar header.
YearSelectClass string? null Applied to the <select> element for year navigation in the calendar header.
DayGridClass string? null Applied to the 7-column grid container holding the day-of-week labels and day cells. The grid layout itself is always present; use this for gap, padding, or text alignment.
DayOfWeekClass string? null Applied to each day-of-week label span (Su, Mo, Tu …).
DayClass string? null Base class applied to every day button.
SelectedDayClass string? null Added to a day button when it is the selected date.
TodayDayClass string? null Added to the day button representing today's date.
OutsideDayClass string? null Reserved for styling days that fall outside the current month grid (e.g. padding cells).
DisabledDayClass string? null Added to day buttons that are disabled due to Min/Max constraints.
RangeStartClass string? null Additional class on the range start day button (IsDateRange only). Applied on top of SelectedDayClass.
RangeEndClass string? null Additional class on the range end day button (IsDateRange only). Applied on top of SelectedDayClass.
InRangeClass string? null Applied to day buttons that fall between the start and end dates. Also applied during hover preview while selecting the end date.
TimeInputClass string? null Applied to the input type="time" element in DateTime and Time modes.
AdditionalAttributes Dictionary<string, object>? null Arbitrary HTML attributes forwarded to the root element (<div> or <input type="time"> in Time mode).

MDatePickerMode

Value Description
Date Calendar only. Commits selected date at midnight.
DateTime Calendar + time input. Preserves time when changing date.
Time Time input only. Stores the time on DateTime.Today.
Best Practices
  • Always pair with MLabel using matching id and For for accessible association
  • Use Class="relative inline-block" (or similar) on the root so the panel can be absolute-positioned below the trigger
  • Add a z-* class to PanelClass to ensure the panel layers above surrounding content
  • Use @bind-Value rather than manual Value + ValueChanged wiring
  • Supply DisabledDayClass with visual cues (opacity, strikethrough) so users understand why dates are unavailable
  • In Time mode, AdditionalAttributes are forwarded to the <input type="time"> directly — you can pass id, aria-label, etc.