Component

date picker

dx components add date_picker

A date picker component to select or input dates.

YYYYMMDD

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;
use dioxus_primitives::{
    calendar::DateRange,
    date_picker::{
        self, DatePickerDaySegmentProps, DatePickerInputProps, DatePickerMonthSegmentProps,
        DatePickerSeparatorProps, DatePickerYearSegmentProps, DateRangePickerEndValueProps,
        DateRangePickerStartValueProps,
    },
    dioxus_attributes::attributes,
    merge_attributes,
    popover::{PopoverContentProps, PopoverTriggerProps},
    ContentAlign,
};
use time::{Date, Month};

use super::super::calendar::*;
use super::super::popover::*;

// docs/backlog.md row 32: `#[css_module]` is gone -- see checkbox/component.rs's
// header comment for the full delivery-mechanism rationale (asset!() +
// document::Link, embedded in the two public entry points -- `DatePicker`
// and `DateRangePicker` -- the same way `select/component.rs` links from
// both of ITS two entry points, since a caller uses exactly one of the two
// and never both).
//
// This component is also on the row-32 "not already namespaced" lane: the
// segment class used to be the bare `dx-date-segment`, not
// `dx-date-picker-segment`. `#[css_module]`'s hash kept it collision-safe
// regardless, but a plain `dx-date-segment` isn't provably this
// component's own once the hash is gone. Renamed to `dx-date-picker-segment`
// in the same change that drops the macro, in both this file and
// `style.css` -- see `scripts/check-dx-class-prefix.sh`.
fn fixed_date(year: i32, month: Month, day: u8) -> Date {
    Date::from_calendar_date(year, month, day).expect("valid fixed date")
}

#[derive(Clone, Copy)]
struct StyledDatePickerContext {
    month_count: u8,
}

fn styled_month_count() -> u8 {
    try_use_context::<StyledDatePickerContext>()
        .map(|ctx| ctx.month_count)
        .unwrap_or(1)
        .max(1)
}

#[derive(Props, Clone, PartialEq)]
pub struct DatePickerProps {
    /// Callback when value changes
    #[props(default)]
    pub on_value_change: Callback<Option<Date>>,

    /// The selected date
    #[props(default)]
    pub selected_date: ReadSignal<Option<Date>>,

    /// Whether the date picker is disabled
    #[props(default)]
    pub disabled: ReadSignal<bool>,

    /// Whether the date picker is enable user input
    #[props(default = ReadSignal::new(Signal::new(false)))]
    pub read_only: ReadSignal<bool>,

    /// 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,

    /// Callback when display day placeholder
    #[props(default = Callback::new(|_| "D".to_string()))]
    pub on_format_day_placeholder: Callback<(), String>,

    /// Callback when display month placeholder
    #[props(default = Callback::new(|_| "M".to_string()))]
    pub on_format_month_placeholder: Callback<(), String>,

    /// Callback when display year placeholder
    #[props(default = Callback::new(|_| "Y".to_string()))]
    pub on_format_year_placeholder: Callback<(), String>,

    /// Locale-aware month name for the month segment's `aria-valuetext`
    /// (backlog row 84 finding 2) -- see
    /// `dioxus_primitives::date_picker::DateElementProps::on_format_month`.
    #[props(default = Callback::new(|month: Month| month.to_string()))]
    pub on_format_month: Callback<Month, String>,

    /// Specify how many months are visible at once
    #[props(default = 1)]
    pub month_count: u8,

    /// Unavailable dates
    #[props(default)]
    pub disabled_ranges: ReadSignal<Vec<DateRange>>,

    /// Whether focus should loop around when reaching the end.
    #[props(default = ReadSignal::new(Signal::new(false)))]
    pub roving_loop: ReadSignal<bool>,

    /// Additional attributes to extend the date picker element
    #[props(extends = GlobalAttributes)]
    pub attributes: Vec<Attribute>,
}

#[derive(Props, Clone, PartialEq)]
pub struct DateRangePickerProps {
    /// Callback when value changes
    #[props(default)]
    pub on_range_change: Callback<Option<DateRange>>,

    /// The selected date
    #[props(default)]
    pub selected_range: ReadSignal<Option<DateRange>>,

    /// Whether the date picker is disabled
    #[props(default)]
    pub disabled: ReadSignal<bool>,

    /// Whether the date picker is enable user input
    #[props(default = ReadSignal::new(Signal::new(false)))]
    pub read_only: ReadSignal<bool>,

    /// 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,

    /// Callback when display day placeholder
    #[props(default = Callback::new(|_| "D".to_string()))]
    pub on_format_day_placeholder: Callback<(), String>,

    /// Callback when display month placeholder
    #[props(default = Callback::new(|_| "M".to_string()))]
    pub on_format_month_placeholder: Callback<(), String>,

    /// Callback when display year placeholder
    #[props(default = Callback::new(|_| "Y".to_string()))]
    pub on_format_year_placeholder: Callback<(), String>,

    /// See `DatePickerProps::on_format_month` (backlog row 84 finding 2).
    #[props(default = Callback::new(|month: Month| month.to_string()))]
    pub on_format_month: Callback<Month, String>,

    /// Specify how many months are visible at once
    #[props(default = 1)]
    pub month_count: u8,

    /// Unavailable dates
    #[props(default)]
    pub disabled_ranges: ReadSignal<Vec<DateRange>>,

    /// Whether focus should loop around when reaching the end.
    #[props(default = ReadSignal::new(Signal::new(false)))]
    pub roving_loop: ReadSignal<bool>,

    /// Additional attributes to extend the date picker element
    #[props(extends = GlobalAttributes)]
    pub attributes: Vec<Attribute>,
}

#[component]
pub fn DatePicker(props: DatePickerProps) -> Element {
    let base = attributes!(div {
        class: "dx-date-picker"
    });
    let merged = merge_attributes(vec![base, props.attributes]);
    let month_count = props.month_count.max(1);
    use_context_provider(|| StyledDatePickerContext { month_count });

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/date_picker/style.css") }
        div {
            date_picker::DatePicker {
                on_value_change: props.on_value_change,
                selected_date: props.selected_date,
                disabled: props.disabled,
                read_only: props.read_only,
                min_date: props.min_date,
                max_date: props.max_date,
                disabled_ranges: props.disabled_ranges,
                roving_loop: props.roving_loop,
                attributes: merged,
                date_picker::DatePickerPopover {
                    // Item 3 fix (2026-09-01, live-site report): matches
                    // `ColorPickerPopover`'s own `is_modal: false`
                    // (`../color_picker/component.rs`) -- a calendar
                    // dropdown, like a color swatch popover, is meant to sit
                    // anchored next to its trigger, not centered as a modal
                    // dialog. Requires `primitives/src/date_picker.rs`'s
                    // `DatePickerPopover` to actually forward `is_modal` to
                    // `PopoverRoot` (previously dropped -- see that fix's
                    // comment) and this page's `style.css` to carry the
                    // `@supports (anchor-name: --a)` block every other
                    // non-modal-arm consumer has, or this would only trade
                    // one broken position for another.
                    popover_root: PopoverRoot,
                    is_modal: false,
                    DatePickerInput {
                        on_format_day_placeholder: props.on_format_day_placeholder,
                        on_format_month_placeholder: props.on_format_month_placeholder,
                        on_format_year_placeholder: props.on_format_year_placeholder,
                        on_format_month: props.on_format_month,
                    }
                }
            }
        }
    }
}

#[component]
pub fn DateRangePicker(props: DateRangePickerProps) -> Element {
    let base = attributes!(div {
        class: "dx-date-picker"
    });
    let merged = merge_attributes(vec![base, props.attributes]);
    let month_count = props.month_count.max(1);
    use_context_provider(|| StyledDatePickerContext { month_count });

    rsx! {
        document::Link { rel: "stylesheet", href: asset!("/src/components/date_picker/style.css") }
        div {
            date_picker::DateRangePicker {
                on_range_change: props.on_range_change,
                selected_range: props.selected_range,
                disabled: props.disabled,
                read_only: props.read_only,
                min_date: props.min_date,
                max_date: props.max_date,
                disabled_ranges: props.disabled_ranges,
                roving_loop: props.roving_loop,
                attributes: merged,
                date_picker::DatePickerPopover {
                    // See the comment on `DatePicker`'s own
                    // `date_picker::DatePickerPopover` above -- identical fix.
                    popover_root: PopoverRoot,
                    is_modal: false,
                    DateRangePickerInput {
                        on_format_day_placeholder: props.on_format_day_placeholder,
                        on_format_month_placeholder: props.on_format_month_placeholder,
                        on_format_year_placeholder: props.on_format_year_placeholder,
                        on_format_month: props.on_format_month,
                    }
                }
            }
        }
    }
}

#[component]
pub(crate) fn DatePickerInput(props: DatePickerInputProps) -> Element {
    let base = attributes!(div {
        class: "dx-date-picker-group"
    });
    let merged = merge_attributes(vec![base, props.attributes]);
    let extra_children = props.children;
    let month_count = styled_month_count();

    rsx! {
        date_picker::DatePickerInput {
            on_format_day_placeholder: props.on_format_day_placeholder,
            on_format_month_placeholder: props.on_format_month_placeholder,
            on_format_year_placeholder: props.on_format_year_placeholder,
            attributes: merged,
            date_picker::DatePickerInputValue {
                on_format_day_placeholder: props.on_format_day_placeholder,
                on_format_month_placeholder: props.on_format_month_placeholder,
                on_format_year_placeholder: props.on_format_year_placeholder,
                on_format_month: props.on_format_month,
                DatePickerYearSegment {}
                DatePickerSeparator {}
                DatePickerMonthSegment {}
                DatePickerSeparator {}
                DatePickerDaySegment {}
            }
            if let Some(extra_children) = extra_children {
                {extra_children}
            }
            DatePickerPopoverTrigger {}
            DatePickerPopoverContent { align: ContentAlign::Center,
                date_picker::DatePickerCalendar { calendar: CalendarRoot,
                    for offset in 0..month_count {
                        CalendarMonthView { key: "{offset}", offset, month_count }
                    }
                }
            }
        }
    }
}

#[component]
pub(crate) fn DateRangePickerInput(props: DatePickerInputProps) -> Element {
    let base = attributes!(div {
        class: "dx-date-picker-group"
    });
    let merged = merge_attributes(vec![base, props.attributes]);
    let extra_children = props.children;
    let month_count = styled_month_count();

    rsx! {
        date_picker::DateRangePickerInput {
            on_format_day_placeholder: props.on_format_day_placeholder,
            on_format_month_placeholder: props.on_format_month_placeholder,
            on_format_year_placeholder: props.on_format_year_placeholder,
            attributes: merged,
            date_picker::DateRangePickerInputValue {
                on_format_day_placeholder: props.on_format_day_placeholder,
                on_format_month_placeholder: props.on_format_month_placeholder,
                on_format_year_placeholder: props.on_format_year_placeholder,
                on_format_month: props.on_format_month,
                DateRangePickerStartValue {
                    DatePickerYearSegment {}
                    DatePickerSeparator {}
                    DatePickerMonthSegment {}
                    DatePickerSeparator {}
                    DatePickerDaySegment {}
                }
                DatePickerSeparator { symbol: '—' }
                DateRangePickerEndValue {
                    DatePickerYearSegment {}
                    DatePickerSeparator {}
                    DatePickerMonthSegment {}
                    DatePickerSeparator {}
                    DatePickerDaySegment {}
                }
            }
            if let Some(extra_children) = extra_children {
                {extra_children}
            }
            DatePickerPopoverTrigger {}
            DatePickerPopoverContent {
                align: ContentAlign::Center,
                date_picker::DateRangePickerCalendar {
                    calendar: RangeCalendarRoot,
                    for offset in 0..month_count {
                        CalendarMonthView { key: "{offset}", offset, month_count }
                    }
                }
            }
        }
    }
}

#[component]
pub(crate) fn DatePickerYearSegment(props: DatePickerYearSegmentProps) -> Element {
    rsx! {
        date_picker::DatePickerYearSegment {
            class: "dx-date-picker-segment",
            attributes: props.attributes,
        }
    }
}

#[component]
pub(crate) fn DatePickerMonthSegment(props: DatePickerMonthSegmentProps) -> Element {
    rsx! {
        date_picker::DatePickerMonthSegment {
            class: "dx-date-picker-segment",
            attributes: props.attributes,
        }
    }
}

#[component]
pub(crate) fn DatePickerDaySegment(props: DatePickerDaySegmentProps) -> Element {
    rsx! {
        date_picker::DatePickerDaySegment {
            class: "dx-date-picker-segment",
            attributes: props.attributes,
        }
    }
}

#[component]
pub(crate) fn DatePickerSeparator(props: DatePickerSeparatorProps) -> Element {
    rsx! {
        date_picker::DatePickerSeparator {
            class: "dx-date-picker-segment",
            symbol: props.symbol,
            attributes: props.attributes,
        }
    }
}

#[component]
pub(crate) fn DateRangePickerStartValue(props: DateRangePickerStartValueProps) -> Element {
    rsx! {
        date_picker::DateRangePickerStartValue {
            {props.children}
        }
    }
}

#[component]
pub(crate) fn DateRangePickerEndValue(props: DateRangePickerEndValueProps) -> Element {
    rsx! {
        date_picker::DateRangePickerEndValue {
            {props.children}
        }
    }
}

#[component]
pub(crate) fn DatePickerPopoverTrigger(props: PopoverTriggerProps) -> Element {
    rsx! {
        PopoverTrigger {
            class: "dx-date-picker-popover-trigger",
            aria_label: "Show Calendar",
            attributes: props.attributes,
            ChevronDown {
                class: "dx-date-picker-trigger",
                size: "20px",
                stroke: "var(--primary-color-7)",
            }
        }
    }
}

#[component]
pub(crate) fn DatePickerPopoverContent(props: PopoverContentProps) -> Element {
    rsx! {
        PopoverContent {
            class: "dx-date-picker-popover-content".to_string(),
            id: props.id,
            side: props.side,
            align: props.align,
            attributes: props.attributes,
            {props.children}
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use dioxus_primitives::calendar::DateRange;

    #[component]
    fn DatePickerWithDefaultInput() -> Element {
        rsx! {
            DatePicker {
                selected_date: Some(fixed_date(2026, Month::May, 7)),
            }
        }
    }

    #[component]
    fn DateRangePickerWithDefaultInput() -> Element {
        rsx! {
            DateRangePicker {
                selected_range: Some(DateRange::new(
                    fixed_date(2026, Month::May, 7),
                    fixed_date(2026, Month::May, 11),
                )),
            }
        }
    }

    #[test]
    fn date_picker_renders_default_input_when_children_are_omitted() {
        let mut dom = VirtualDom::new(DatePickerWithDefaultInput);
        dom.rebuild_in_place();
        let html = dioxus_ssr::render(&dom);

        assert!(html.contains("2026"));
        assert!(html.contains("05"));
        assert!(html.contains("07"));
        assert!(html.contains("Show Calendar"));
    }

    #[test]
    fn date_range_picker_renders_default_input_when_children_are_omitted() {
        let mut dom = VirtualDom::new(DateRangePickerWithDefaultInput);
        dom.rebuild_in_place();
        let html = dioxus_ssr::render(&dom);

        assert!(html.contains("2026"));
        assert!(html.contains("05"));
        assert!(html.contains("07"));
        assert!(html.contains("11"));
        assert!(html.contains("Show Calendar"));
    }

}

Usage notes

The DatePicker component is used to display a date input and a Calendar popover, allowing users to enter or select a date value.

Component Structure

DatePicker {
    // The currently selected date in the date picker (if any).
    selected_date,
    on_value_change: move |v: Option<Date>| {
        // This callback is triggered when a date is selected in the
        // calendar or the user entered it from the keyboard.
        // The date parameter contains the selected date.
    },
    // Optional number of pre-composed calendar months to show in the popover.
    month_count: 1,
    // Optional placeholder formatters for the input fields.
    on_format_day_placeholder: || "D",
    on_format_month_placeholder: || "M",
    on_format_year_placeholder: || "Y",
}

The styled DatePicker and DateRangePicker render the input, trigger, popover content, and calendar by default.

Variants

Alternative examples for common configurations.

internationalized

YYYYMMDD

range

YYYYMMDDYYYYMMDD

multi_month

YYYYMMDDYYYYMMDD

unavailable_dates

YYYYMMDDYYYYMMDD