Date picker: Bounds and blocked days
The slotsmith date picker limits what can be picked in two ways: minDate and maxDate set the earliest and the latest date, and disabledDates blocks single days such as weekends or booked nights. Blocked days stay focusable, but cannot be picked.
Appointment
Appointment: none
Confirmed on
import { useId, useState } from "react";
import { addMonths, DatePicker, fromISODate, type ISODate } from "slotsmith/date-picker";
import { useToday } from "../shared/today";
/** Saturday or Sunday, read from the date itself. */
const isWeekend = (date: ISODate) => {
const day = fromISODate(date)?.getDay();
return day === 0 || day === 6;
};
export default function BoundsAndBlockedDays() {
const id = useId();
const today = useToday();
const [visit, setVisit] = useState<ISODate | null>(null);
return (
<div style={{ display: "grid", gap: "1rem", maxWidth: 288 }}>
<div>
<p id={`${id}-visit`}>Appointment</p>
<DatePicker
value={visit}
onChange={setVisit}
// Nothing before today or more than two months ahead; the calendar stops at both months.
minDate={today}
maxDate={addMonths(today, 2)}
// Weekends stay focusable and are announced, but cannot be picked.
disabledDates={isWeekend}
aria-labelledby={`${id}-visit`}
/>
<p>Appointment: {visit ?? "none"}</p>
</div>
<div>
<p id={`${id}-locked`}>Confirmed on</p>
{/* `disabled` turns off the whole control, not single days. */}
<DatePicker disabled defaultValue="2026-03-12" aria-labelledby={`${id}-locked`} />
</div>
</div>
);
}import { useSyncExternalStore } from "react";
import { today, type ISODate } from "slotsmith/date-picker";
/** The date a prerendered page is built with, so its HTML is the same on every build. */
export const PRERENDER_TODAY: ISODate = "2026-03-16";
// Today does not change while the page is open, so there is nothing to subscribe to.
const subscribe = () => () => {};
/**
* Today as `YYYY-MM-DD`, safe to prerender: the fixed date on the server and
* while hydrating, so the markup matches, then the visitor's real date.
* An app that renders only in the browser can call `today()` directly.
*/
export function useToday(): ISODate {
return useSyncExternalStore(subscribe, today, () => PRERENDER_TODAY);
}The appointment can be any weekday from today to two months ahead. Open it and move with the arrow keys: focus crosses the weekends, and stops at today.
minDate and maxDate
minDate and maxDate are days written like the value (an ISO instant by default, or as your value format says; a plain "2026-03-12" is read too), both inclusive. With them set:
- Days outside the bounds cannot be picked.
- The calendar does not go past the bounds’ months: the previous and next buttons are disabled there.
- The arrow keys, PgUp and PgDn stop at the bounds.
- The year select lists the bounds’ years only. Without bounds it lists ten years either side of the year on screen.
disabledDates
disabledDates is a function that receives a date and returns true to block it. The demo blocks Saturdays and Sundays; a set of booked dates works the same way: (date) => booked.has(date).
A blocked day, like a day outside the bounds, gets aria-disabled="true" and data-disabled, never the disabled attribute. It stays focusable, so the arrow keys cross it and a screen reader announces it. A click or Enter on it does nothing.
In range mode, disabledDates stops a blocked day from being picked as the start or the end. It does not stop a range from spanning blocked days. If a stay must not include a booked night, check the range in onChange.
Disabling the whole control
disabled is separate from disabledDates: it turns off the whole picker, like the second one in the demo. The trigger gets aria-disabled="true", leaves the tab order, does not open, and shows no clear button.