Component
calendar
dx components add calendarA calendar grid component for selecting dates.
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
Installation
Use the CLI command for the common path, or copy the component files manually.
Manual installation files
Copy the component source and CSS into your app. Import the shared theme CSS once near your app root.
use dioxus::prelude::*;
use dioxus_icons::lucide::{ChevronDown, ChevronLeft, ChevronRight};
use dioxus_primitives::calendar::{
self, CalendarDayProps, CalendarGridBodyProps, CalendarGridCellProps,
CalendarGridDayHeaderProps, CalendarGridHeadProps, CalendarGridHeaderRowProps,
CalendarGridRootProps, CalendarGridWeekProps, CalendarHeaderProps, CalendarNavigationProps,
CalendarSelectMonthProps, CalendarSelectMonthSelectProps, CalendarSelectMonthValueProps,
CalendarSelectYearProps, CalendarSelectYearSelectProps, CalendarSelectYearValueProps,
CalendarViewProps,
};
use dioxus_primitives::{dioxus_attributes::attributes, merge_attributes};
use time::{Date, Month, UtcDateTime, Weekday};
fn fixed_date(year: i32, month: Month, day: u8) -> Date {
Date::from_calendar_date(year, month, day).expect("valid fixed date")
}
fn weekday_abbreviation(weekday: Weekday) -> &'static str {
match weekday {
Weekday::Monday => "Mo",
Weekday::Tuesday => "Tu",
Weekday::Wednesday => "We",
Weekday::Thursday => "Th",
Weekday::Friday => "Fr",
Weekday::Saturday => "Sa",
Weekday::Sunday => "Su",
}
}
#[derive(Props, Clone, PartialEq)]
pub struct CalendarProps {
/// The selected date
#[props(default)]
pub selected_date: ReadSignal<Option<Date>>,
/// Callback when selected date changes
#[props(default)]
pub on_date_change: Callback<Option<Date>>,
/// Callback when display weekday
#[props(default = Callback::new(|weekday: Weekday| weekday_abbreviation(weekday).to_string()))]
pub on_format_weekday: Callback<Weekday, String>,
/// Callback when display month
#[props(default = Callback::new(|month: Month| month.to_string()))]
pub on_format_month: Callback<Month, String>,
/// The month being viewed
#[props(default = ReadSignal::new(Signal::new(UtcDateTime::now().date())))]
pub view_date: ReadSignal<Date>,
/// The current date (used for highlighting today)
#[props(default = UtcDateTime::now().date())]
pub today: Date,
/// Callback when view date changes
#[props(default)]
pub on_view_change: Callback<Date>,
/// Whether the calendar is disabled
#[props(default)]
pub disabled: ReadSignal<bool>,
/// First day of the week
#[props(default = Weekday::Sunday)]
pub first_day_of_week: Weekday,
/// Lower limit of the range of available dates
#[props(default = fixed_date(1925, Month::January, 1))]
pub min_date: Date,
/// Upper limit of the range of available dates
#[props(default = fixed_date(2050, Month::December, 31))]
pub max_date: Date,
/// Specify how many months are visible at once
#[props(default = 1)]
pub month_count: u8,
/// Unavailable dates
#[props(default)]
pub disabled_ranges: ReadSignal<Vec<calendar::DateRange>>,
/// Additional attributes to extend the calendar element
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
}
#[derive(Props, Clone, PartialEq)]
pub struct RangeCalendarProps {
/// The selected range
#[props(default)]
pub selected_range: ReadSignal<Option<calendar::DateRange>>,
/// Callback when selected date range changes
#[props(default)]
pub on_range_change: Callback<Option<calendar::DateRange>>,
/// Callback when display weekday
#[props(default = Callback::new(|weekday: Weekday| weekday_abbreviation(weekday).to_string()))]
pub on_format_weekday: Callback<Weekday, String>,
/// Callback when display month
#[props(default = Callback::new(|month: Month| month.to_string()))]
pub on_format_month: Callback<Month, String>,
/// The month being viewed
#[props(default = ReadSignal::new(Signal::new(UtcDateTime::now().date())))]
pub view_date: ReadSignal<Date>,
/// The current date (used for highlighting today)
#[props(default = UtcDateTime::now().date())]
pub today: Date,
/// Callback when view date changes
#[props(default)]
pub on_view_change: Callback<Date>,
/// Whether the calendar is disabled
#[props(default)]
pub disabled: ReadSignal<bool>,
/// First day of the week
#[props(default = Weekday::Sunday)]
pub first_day_of_week: Weekday,
/// Lower limit of the range of available dates
#[props(default = fixed_date(1925, Month::January, 1))]
pub min_date: Date,
/// Upper limit of the range of available dates
#[props(default = fixed_date(2050, Month::December, 31))]
pub max_date: Date,
/// Specify how many months are visible at once
#[props(default = 1)]
pub month_count: u8,
/// Unavailable dates
#[props(default)]
pub disabled_ranges: ReadSignal<Vec<calendar::DateRange>>,
/// Additional attributes to extend the calendar element
#[props(extends = GlobalAttributes)]
pub attributes: Vec<Attribute>,
}
#[component]
pub fn Calendar(props: CalendarProps) -> Element {
let month_count = props.month_count.max(1);
rsx! {
CalendarRoot {
selected_date: props.selected_date,
on_date_change: props.on_date_change,
on_format_weekday: props.on_format_weekday,
on_format_month: props.on_format_month,
view_date: props.view_date,
today: props.today,
on_view_change: props.on_view_change,
disabled: props.disabled,
first_day_of_week: props.first_day_of_week,
min_date: props.min_date,
max_date: props.max_date,
disabled_ranges: props.disabled_ranges,
attributes: props.attributes,
for offset in 0..month_count {
CalendarMonthView { key: "{offset}", offset, month_count }
}
}
}
}
#[component]
pub fn RangeCalendar(props: RangeCalendarProps) -> Element {
let month_count = props.month_count.max(1);
rsx! {
RangeCalendarRoot {
selected_range: props.selected_range,
on_range_change: props.on_range_change,
on_format_weekday: props.on_format_weekday,
on_format_month: props.on_format_month,
view_date: props.view_date,
today: props.today,
on_view_change: props.on_view_change,
disabled: props.disabled,
first_day_of_week: props.first_day_of_week,
min_date: props.min_date,
max_date: props.max_date,
disabled_ranges: props.disabled_ranges,
attributes: props.attributes,
for offset in 0..month_count {
CalendarMonthView { key: "{offset}", offset, month_count }
}
}
}
}
// docs/backlog.md row 51: the stylesheet `Link` lives HERE, on the
// `pub(crate)` root each of this module's real entry points funnels
// through, not on `Calendar`/`RangeCalendar` above -- `date_picker`
// (`../date_picker/component.rs`) composes `CalendarRoot`/
// `RangeCalendarRoot` directly (`super::super::calendar::*`), bypassing
// `Calendar`/`RangeCalendar` entirely, since `DatePickerCalendar`'s
// `calendar: fn(T) -> Element` prop needs a bare component function to call,
// not a wrapper that also takes a `month_count` prop. Before this fix the
// Link sat only on `Calendar`/`RangeCalendar`, so that composition path
// never rendered it and the calendar's CSS silently never reached
// `/component/?name=date_picker&` -- root-caused by comparing `Calendar`'s
// own route (styled) against `date_picker`'s composition of it (unstyled).
// Putting it here instead -- matching `popover/component.rs`'s pattern of a
// `Link` on every one of ITS composable entry points, not just its "main"
// one -- means every path that ends up rendering the primitive (this
// module's own `Calendar`/`RangeCalendar`, and any other themed component
// that composes `CalendarRoot`/`RangeCalendarRoot` the way `date_picker`
// does) gets the stylesheet for free, by construction; `document::Link`'s
// `(href, rel)` dedup makes the now-redundant render from `Calendar`'s own
// `document::Link` (removed above) unnecessary rather than merely harmless.
#[component]
pub(crate) fn CalendarRoot(props: calendar::CalendarProps) -> Element {
let base = attributes!(div {
class: "dx-calendar"
});
let merged = merge_attributes(vec![base, props.attributes]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/calendar/style.css") }
calendar::Calendar {
selected_date: props.selected_date,
on_date_change: props.on_date_change,
on_format_weekday: props.on_format_weekday,
on_format_month: props.on_format_month,
view_date: props.view_date,
today: props.today,
on_view_change: props.on_view_change,
disabled: props.disabled,
first_day_of_week: props.first_day_of_week,
min_date: props.min_date,
max_date: props.max_date,
disabled_ranges: props.disabled_ranges,
attributes: merged,
{props.children}
}
}
}
// See the comment on `CalendarRoot` above -- identical reasoning, for the
// range-calendar arm `date_picker::DateRangePickerCalendar` composes.
#[component]
pub(crate) fn RangeCalendarRoot(props: calendar::RangeCalendarProps) -> Element {
let base = attributes!(div {
class: "dx-calendar"
});
let merged = merge_attributes(vec![base, props.attributes]);
rsx! {
document::Link { rel: "stylesheet", href: asset!("/src/components/calendar/style.css") }
calendar::RangeCalendar {
selected_range: props.selected_range,
on_range_change: props.on_range_change,
on_format_weekday: props.on_format_weekday,
on_format_month: props.on_format_month,
view_date: props.view_date,
today: props.today,
on_view_change: props.on_view_change,
disabled: props.disabled,
first_day_of_week: props.first_day_of_week,
min_date: props.min_date,
max_date: props.max_date,
disabled_ranges: props.disabled_ranges,
attributes: merged,
{props.children}
}
}
}
#[component]
pub(crate) fn CalendarMonthView(offset: u8, month_count: u8) -> Element {
let show_previous = offset == 0;
let show_next = offset.saturating_add(1) == month_count;
rsx! {
CalendarView { offset,
CalendarHeader {
CalendarNavigation {
if show_previous {
CalendarPreviousMonthButton {}
}
CalendarSelectMonth {}
CalendarSelectYear {}
if show_next {
CalendarNextMonthButton {}
}
}
}
CalendarGrid {}
}
}
}
#[component]
fn CalendarView(props: CalendarViewProps) -> Element {
rsx! {
calendar::CalendarView {
class: "dx-calendar-view",
offset: props.offset,
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarHeader(props: CalendarHeaderProps) -> Element {
rsx! {
calendar::CalendarHeader { id: props.id, attributes: props.attributes, {props.children} }
}
}
#[component]
fn CalendarNavigation(props: CalendarNavigationProps) -> Element {
rsx! {
calendar::CalendarNavigation { class: "dx-calendar-navigation", attributes: props.attributes, {props.children} }
}
}
#[component]
fn CalendarPreviousMonthButton(
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
rsx! {
calendar::CalendarPreviousMonthButton { class: "dx-calendar-nav-prev", attributes,
ChevronLeft { size: "20px" }
}
}
}
#[component]
fn CalendarNextMonthButton(
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
rsx! {
calendar::CalendarNextMonthButton { class: "dx-calendar-nav-next", attributes,
ChevronRight { size: "20px" }
}
}
}
#[component]
fn CalendarSelectMonth(props: CalendarSelectMonthProps) -> Element {
rsx! {
calendar::CalendarSelectMonth {
attributes: props.attributes,
class: "dx-calendar-month-select-container",
CalendarSelectMonthSelect {}
CalendarSelectMonthValue {
DropDownIcon { }
{props.children}
}
}
}
}
#[component]
fn CalendarSelectMonthSelect(props: CalendarSelectMonthSelectProps) -> Element {
rsx! {
calendar::CalendarSelectMonthSelect {
class: "dx-calendar-month-select",
attributes: props.attributes,
}
}
}
#[component]
fn CalendarSelectMonthValue(props: CalendarSelectMonthValueProps) -> Element {
rsx! {
calendar::CalendarSelectMonthValue {
class: "dx-calendar-month-select-value",
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarSelectYear(props: CalendarSelectYearProps) -> Element {
rsx! {
calendar::CalendarSelectYear {
attributes: props.attributes,
class: "dx-calendar-year-select-container",
CalendarSelectYearSelect {}
CalendarSelectYearValue {
DropDownIcon { }
{props.children}
}
}
}
}
#[component]
fn CalendarSelectYearSelect(props: CalendarSelectYearSelectProps) -> Element {
rsx! {
calendar::CalendarSelectYearSelect {
class: "dx-calendar-year-select",
attributes: props.attributes,
}
}
}
#[component]
fn CalendarSelectYearValue(props: CalendarSelectYearValueProps) -> Element {
rsx! {
calendar::CalendarSelectYearValue {
class: "dx-calendar-year-select-value",
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGrid(
#[props(default)] id: Option<String>,
#[props(extends = GlobalAttributes)] attributes: Vec<Attribute>,
) -> Element {
let grid = calendar::use_calendar_grid();
rsx! {
CalendarGridRoot { id, attributes,
CalendarGridHead {
CalendarGridHeaderRow {
for weekday in grid.weekdays().iter().cloned() {
CalendarGridDayHeader {
key: "{weekday.weekday():?}",
weekday: weekday.weekday(),
{weekday.label().to_string()}
}
}
}
}
CalendarGridBody {
for week in grid.weeks() {
CalendarGridWeek {
for date in week.iter().copied() {
CalendarGridCell {
key: "{date}",
CalendarDay { date }
}
}
}
}
}
}
}
}
#[component]
fn CalendarGridRoot(props: CalendarGridRootProps) -> Element {
rsx! {
calendar::CalendarGridRoot {
class: "dx-calendar-grid",
id: props.id,
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridHead(props: CalendarGridHeadProps) -> Element {
rsx! {
calendar::CalendarGridHead {
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridHeaderRow(props: CalendarGridHeaderRowProps) -> Element {
rsx! {
calendar::CalendarGridHeaderRow {
class: "dx-calendar-grid-header",
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridDayHeader(props: CalendarGridDayHeaderProps) -> Element {
rsx! {
calendar::CalendarGridDayHeader {
class: "dx-calendar-grid-day-header",
weekday: props.weekday,
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridBody(props: CalendarGridBodyProps) -> Element {
rsx! {
calendar::CalendarGridBody {
class: "dx-calendar-grid-body",
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridWeek(props: CalendarGridWeekProps) -> Element {
rsx! {
calendar::CalendarGridWeek {
class: "dx-calendar-grid-week",
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarGridCell(props: CalendarGridCellProps) -> Element {
rsx! {
calendar::CalendarGridCell {
attributes: props.attributes,
{props.children}
}
}
}
#[component]
fn CalendarDay(props: CalendarDayProps) -> Element {
rsx! {
calendar::CalendarDay {
class: "dx-calendar-grid-cell",
date: props.date,
attributes: props.attributes,
children: props.children,
}
}
}
#[component]
fn DropDownIcon() -> Element {
rsx! {
ChevronDown {
size: "20px",
stroke: "var(--secondary-color-4)",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[component]
fn CalendarWithDefaultView() -> Element {
rsx! {
Calendar {
view_date: fixed_date(2026, Month::May, 15),
}
}
}
#[component]
fn CalendarWithDefaultMonthCount() -> Element {
rsx! {
Calendar {
view_date: fixed_date(2026, Month::May, 15),
month_count: 3,
}
}
}
#[test]
fn calendar_renders_default_view_when_children_are_omitted() {
let mut dom = VirtualDom::new(CalendarWithDefaultView);
dom.rebuild_in_place();
let html = dioxus_ssr::render(&dom);
assert!(html.contains("Calendar"));
assert_eq!(html.matches("role=\"grid\"").count(), 1);
}
#[test]
fn calendar_month_count_renders_multiple_default_views() {
let mut dom = VirtualDom::new(CalendarWithDefaultMonthCount);
dom.rebuild_in_place();
let html = dioxus_ssr::render(&dom);
assert_eq!(html.matches("role=\"grid\"").count(), 3);
}
}Usage notes
The Calendar component is used to display a calendar interface, allowing users to select dates. It provides a grid layout of days for a specific month and year.
Component Structure
Calendar {
// The currently selected date in the calendar (if any).
selected_date,
on_date_change: |date: Option<CalendarDate>| {
// This callback is triggered when a date is selected in the calendar.
// The date parameter contains the selected date.
},
// The current view date of the calendar, which determines the month and year displayed.
view_date,
on_view_change: |date: CalendarDate| {
// This callback is triggered when the view date changes.
// The date parameter contains the new view date.
},
// Optional number of pre-composed month views to render.
month_count: 1,
}The styled Calendar and RangeCalendar render a complete month view by default, including the header, navigation, month/year selectors, and date grid.
Direction / RTL
Calendar/RangeCalendar accept a dir: Option<Direction> prop. Under RTL, the day grid's ArrowLeft/ArrowRight swap (ArrowLeft moves to the next day, ArrowRight to the previous one); ArrowUp/ArrowDown (+/-7 days) are unaffected. No Radix Calendar exists to cite (shadcn's own wraps react-day-picker) -- this follows the same arrow-key convention as every other RTL-aware component here, and matches standard calendar-widget RTL practice generally. See the rtl variant.
Variants
Alternative examples for common configurations.
simple
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
internationalized
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
range
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
multi_month
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|
unavailable_dates
rtl
| Su | Mo | Tu | We | Th | Fr | Sa |
|---|---|---|---|---|---|---|