Datepicker Composition
Learn how to compose different styles of datepickers using SummitUI's headless components.
SummitUI's headless architecture allows you to combine Calendar, Popover, and DateField components to create various
datepicker experiences. This guide demonstrates two common patterns and how to integrate them with EditForm.
Popover Datepicker
A simple datepicker button that opens a calendar popover. This pattern is great for forms where space is limited.
<div class="flex items-start">
<SmPopoverRoot @bind-Open="_isPopoverOpen">
<SmPopoverTrigger class="inline-flex h-10 w-[240px] items-center justify-start text-left font-normal rounded-md border border-su-border bg-su-background px-3 py-2 text-sm ring-offset-su-background hover:bg-su-accent hover:text-su-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-su-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" aria-label="Pick a date">
<!-- Calendar Icon -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mr-2 h-4 w-4">
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" />
</svg>
@if (_date.HasValue)
{
<span>@_date.Value.ToString("yyyy-MM-dd")</span>
}
else
{
<span class="text-su-muted-foreground">Pick a date</span>
}
</SmPopoverTrigger>
<SmPopoverPortal>
<SmPopoverContent class="w-auto p-0 bg-su-card text-su-card-foreground border border-su-border rounded-md shadow-md outline-none" Align="Align.Start" SideOffset="4">
<SmCalendarRoot Value="_date" ValueChanged="OnDateChanged" class="p-3" Context="ctx">
<SmCalendarHeader class="flex items-center justify-between pt-1 relative pb-4">
<SmCalendarPrevButton class="h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 flex items-center justify-center rounded-md border border-su-border hover:bg-su-accent" />
<SmCalendarHeading class="text-sm font-medium" />
<SmCalendarNextButton class="h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100 flex items-center justify-center rounded-md border border-su-border hover:bg-su-accent" />
</SmCalendarHeader>
<SmCalendarGrid class="w-full border-collapse space-y-1">
<SmCalendarGridHead>
<SmCalendarGridRow class="flex">
@if (ctx.Weekdays != null)
{
@for (int i = 0; i < ctx.Weekdays.Length; i++)
{
var idx = i;
<SmCalendarHeadCell Abbreviation="@ctx.WeekdaysLong[idx]" class="text-su-muted-foreground rounded-md w-9 font-normal text-[0.8rem]">
@ctx.Weekdays[idx]
</SmCalendarHeadCell>
}
}
</SmCalendarGridRow>
</SmCalendarGridHead>
<SmCalendarGridBody>
@if (ctx.CurrentMonth != null)
{
@foreach (var week in ctx.CurrentMonth.Weeks)
{
<SmCalendarGridRow class="flex w-full mt-2">
@foreach (var date in week)
{
<SmCalendarCell Date="@date" class="relative p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([aria-selected])]:bg-su-accent [&:has([aria-selected].day-outside)]:bg-su-accent/50 [&:has([aria-selected].day-range-end)]:rounded-r-md">
<SmCalendarDay class="@DayClass" />
</SmCalendarCell>
}
</SmCalendarGridRow>
}
}
</SmCalendarGridBody>
</SmCalendarGrid>
</SmCalendarRoot>
</SmPopoverContent>
</SmPopoverPortal>
</SmPopoverRoot>
</div>
@code {
private DateOnly? _date;
private bool _isPopoverOpen;
private void OnDateChanged(DateOnly? newDate)
{
_date = newDate;
_isPopoverOpen = false; // Close popover on selection
}
// Tailwind classes for the day button
private string DayClass => "h-9 w-9 p-0 font-normal aria-selected:opacity-100 hover:bg-su-accent hover:text-su-accent-foreground focus:bg-su-accent focus:text-su-accent-foreground rounded-md transition-colors data-[today]:bg-su-accent data-[today]:text-su-accent-foreground data-[selected]:bg-su-primary data-[selected]:text-su-primary-foreground data-[selected]:hover:bg-su-primary data-[selected]:hover:text-su-primary-foreground data-[selected]:focus:bg-su-primary data-[selected]:focus:text-su-primary-foreground data-[outside-month]:text-su-muted-foreground data-[outside-month]:opacity-50 data-[disabled]:text-su-muted-foreground data-[disabled]:opacity-50";
}DateField with Calendar
Combines a keyboard-friendly DateField input with a calendar
button. This offers the best accessibility and usability, allowing users to type the date or pick it
visually.
<div class="flex items-start gap-2">
<SmPopoverRoot @bind-Open="_isPopoverOpen">
<SmDateFieldRoot @bind-Value="_date" Format="yyyy-MM-dd" class="group">
<SmDateFieldLabel class="sr-only">Date</SmDateFieldLabel>
<SmDateFieldInput
class="flex flex-row gap-4 justify-between h-10 w-full items-center rounded-md border border-su-border bg-su-background pl-3 py-2 text-sm ring-offset-su-background focus-within:ring-2 focus-within:ring-su-ring focus-within:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50">
<div class="flex flex-row gap-1 items-center">
@foreach (var segment in context)
{
<SmDateFieldSegment Segment="@segment"/>
}
</div>
<SmPopoverTrigger
class="inline-flex h-8 w-8 items-center justify-center rounded-md bg-su-background text-su-foreground hover:bg-su-accent hover:text-su-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-su-ring focus-visible:ring-offset-2"
aria-label="Open calendar">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="18" height="18" x="3" y="4" rx="2" ry="2"/>
<line x1="16" y1="2" x2="16" y2="6"/>
<line x1="8" y1="2" x2="8" y2="6"/>
<line x1="3" y1="10" x2="21" y2="10"/>
</svg>
</SmPopoverTrigger>
</SmDateFieldInput>
</SmDateFieldRoot>
<SmPopoverPortal>
<SmPopoverContent
class="w-auto p-0 bg-su-card text-su-card-foreground border border-su-border rounded-md shadow-md outline-none"
Align="Align.End" SideOffset="4">
<SmCalendarRoot Value="_date" ValueChanged="OnDateChanged" class="p-3" Context="ctx">
<!-- Calendar content same as above -->
</SmCalendarRoot>
</SmPopoverContent>
</SmPopoverPortal>
</SmPopoverRoot>
</div>EditForm Integration
While DateField works with EditForm out of the box (as it acts as
an input), the Popover+Calendar pattern requires a custom wrapper to work as a form component.
Native DateField Support
The DateField component implements
InputBase logic internally when you
use @bind-Value, making it fully
compatible with Blazor's validation system.
<EditForm Model="_model">
<DataAnnotationsValidator />
<SmDateFieldRoot @bind-Value="_model.Date">
<!-- DateField internals -->
</SmDateFieldRoot>
<ValidationMessage For="@(() => _model.Date)" />
</EditForm>Custom InputBase Wrapper
To use the Popover+Calendar pattern in an EditForm,
create a wrapper component that inherits from InputBase<DateOnly?>.
// FormDatepicker.razor
@using Microsoft.AspNetCore.Components.Forms
@inherits InputBase<DateOnly?>
<div class="@Class">
<SmPopoverRoot @bind-Open="_isOpen">
<SmPopoverTrigger class="@GetTriggerClass()" ...>
<!-- Trigger content showing CurrentValue -->
</SmPopoverTrigger>
<SmPopoverPortal>
<SmPopoverContent ...>
<SmCalendarRoot Value="CurrentValue" ValueChanged="OnValueChanged" ... />
</SmPopoverContent>
</SmPopoverPortal>
</SmPopoverRoot>
</div>
@code {
private bool _isOpen;
[Parameter] public string? Class { get; set; }
private void OnValueChanged(DateOnly? newValue)
{
CurrentValue = newValue; // Updates the bound value and triggers validation
_isOpen = false;
}
private string GetTriggerClass()
{
// Add styling based on validation state
if (!string.IsNullOrEmpty(CssClass) && CssClass.Contains("invalid"))
{
return "... border-su-destructive text-su-destructive ...";
}
return "...";
}
protected override bool TryParseValueFromString(string? value, out DateOnly? result, out string? validationErrorMessage)
{
// Not used for DateOnly? binding but required by InputBase
result = null;
validationErrorMessage = null;
return true;
}
}