Components / Inputs & Forms
Date Picker
Five interactive date selection styles: inline calendar, popover input, range picker, compact chip, and full calendar with prominent navigation.
1. Single Date Picker
Input field that opens a calendar popover on click. Select a date to close automatically. Dismiss with Escape or by clicking outside.
2. Date Range Picker
First click sets the start date, second click sets the end date and closes. Range is highlighted as you hover. Shows as "May 27 – Jun 3".
Source DatePickerDemo.tsx
The exact code behind the live demo above. Fetch it raw at /components/source/date-picker.txt.
import React, { useState, useRef, useEffect } from "react";
// ---------------------------------------------------------------------------
// Calendar utilities (no external date library)
// ---------------------------------------------------------------------------
const MONTH_NAMES = [
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December",
];
const SHORT_MONTH_NAMES = [
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
];
const DAY_NAMES = ["Mo", "Tu", "We", "Th", "Fr", "Sa", "Su"];
function getDaysInMonth(year: number, month: number): number {
return new Date(year, month + 1, 0).getDate();
}
function getMonthStartOffset(year: number, month: number): number {
const day = new Date(year, month, 1).getDay();
return day === 0 ? 6 : day - 1;
}
function isSameDay(a: Date | null, b: Date | null): boolean {
if (!a || !b) return false;
return (
a.getFullYear() === b.getFullYear() &&
a.getMonth() === b.getMonth() &&
a.getDate() === b.getDate()
);
}
function isToday(date: Date): boolean {
return isSameDay(date, new Date());
}
function isBetween(date: Date, start: Date | null, end: Date | null): boolean {
if (!start || !end) return false;
const t = date.getTime();
const s = new Date(start.getFullYear(), start.getMonth(), start.getDate()).getTime();
const e = new Date(end.getFullYear(), end.getMonth(), end.getDate()).getTime();
return t > s && t < e;
}
function formatInputValue(date: Date | null): string {
if (!date) return "";
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${d} / ${m} / ${y}`;
}
function formatRangeDisplay(start: Date | null, end: Date | null): string {
if (!start) return "";
const s = `${SHORT_MONTH_NAMES[start.getMonth()]} ${start.getDate()}`;
if (!end) return `${s} –`;
const e = `${SHORT_MONTH_NAMES[end.getMonth()]} ${end.getDate()}`;
return `${s} – ${e}`;
}
// ---------------------------------------------------------------------------
// Inject popover keyframes once
// ---------------------------------------------------------------------------
function usePopoverKeyframes() {
useEffect(() => {
const id = "flux-datepicker-keyframes";
if (document.getElementById(id)) return;
const s = document.createElement("style");
s.id = id;
s.textContent = `
@keyframes flux-pop-in {
from { opacity: 0; transform: translateY(-6px) scale(0.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
`;
document.head.appendChild(s);
}, []);
}
// ---------------------------------------------------------------------------
// Shared style helpers
// ---------------------------------------------------------------------------
const labelStyle: React.CSSProperties = {
display: "block",
fontSize: "0.875rem",
fontWeight: 500,
color: "var(--flux-black)",
marginBottom: "4px",
};
function triggerStyle(open: boolean, minWidth = 200): React.CSSProperties {
return {
display: "flex",
alignItems: "center",
gap: "8px",
padding: "8px 12px",
minWidth,
background: "var(--flux-surface)",
border: open
? "1px solid var(--flux-primary-400)"
: "1px solid var(--flux-grey-200)",
borderRadius: "var(--flux-radius-sm)",
cursor: "pointer",
fontSize: "0.875rem",
fontFamily: "inherit",
transition: "border-color 150ms, box-shadow 150ms",
boxShadow: open ? "0 0 0 1px var(--flux-primary-300)" : "none",
outline: "none",
};
}
const popoverStyle: React.CSSProperties = {
position: "absolute",
top: "calc(100% + 6px)",
left: 0,
background: "var(--flux-surface)",
border: "1px solid var(--flux-grey-100)",
borderRadius: "var(--flux-radius-lg)",
boxShadow: "var(--flux-shadow-lg)",
padding: "16px",
zIndex: 50,
minWidth: "268px",
animation: "flux-pop-in 160ms cubic-bezier(0.16, 1, 0.3, 1) forwards",
};
const clearLinkStyle: React.CSSProperties = {
fontSize: "0.75rem",
fontWeight: 500,
color: "var(--flux-grey-400, #9ca3af)",
background: "none",
border: "none",
cursor: "pointer",
padding: "0",
fontFamily: "inherit",
textDecoration: "underline",
textUnderlineOffset: "2px",
};
// ---------------------------------------------------------------------------
// CalendarIcon
// ---------------------------------------------------------------------------
function CalendarIcon() {
return (
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
style={{ flexShrink: 0 }}
>
<rect
x="1.5"
y="2.5"
width="13"
height="12"
rx="2"
stroke="currentColor"
strokeWidth="1.25"
/>
<path
d="M5 1V4M11 1V4M1.5 6.5H14.5"
stroke="currentColor"
strokeWidth="1.25"
strokeLinecap="round"
/>
</svg>
);
}
// ---------------------------------------------------------------------------
// Shared CalendarGrid
// ---------------------------------------------------------------------------
interface CalendarGridProps {
year: number;
month: number;
selected?: Date | null;
rangeStart?: Date | null;
rangeEnd?: Date | null;
hovered?: Date | null;
onSelect: (date: Date) => void;
onHover?: (date: Date | null) => void;
}
function CalendarGrid({
year,
month,
selected,
rangeStart,
rangeEnd,
hovered,
onSelect,
onHover,
}: CalendarGridProps) {
const daysInMonth = getDaysInMonth(year, month);
const offset = getMonthStartOffset(year, month);
const cells: (number | null)[] = [
...Array(offset).fill(null),
...Array.from({ length: daysInMonth }, (_, i) => i + 1),
];
while (cells.length % 7 !== 0) cells.push(null);
const cellSize = 34;
return (
<div style={{ userSelect: "none" }}>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(7, ${cellSize}px)`,
marginBottom: "3px",
}}
>
{DAY_NAMES.map((d) => (
<div
key={d}
style={{
width: cellSize,
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.6875rem",
fontWeight: 700,
color: "var(--flux-grey-300)",
letterSpacing: "0.03em",
}}
>
{d}
</div>
))}
</div>
<div
style={{
display: "grid",
gridTemplateColumns: `repeat(7, ${cellSize}px)`,
gap: "2px",
}}
>
{cells.map((day, idx) => {
if (!day) {
return <div key={idx} style={{ width: cellSize, height: cellSize }} />;
}
const thisDate = new Date(year, month, day);
const isSelected = isSameDay(thisDate, selected ?? null);
const isRangeStart = isSameDay(thisDate, rangeStart ?? null);
const isRangeEnd = isSameDay(thisDate, rangeEnd ?? null);
const effectiveEnd = rangeEnd ?? (rangeStart && hovered ? hovered : null);
const isInRange = isBetween(thisDate, rangeStart ?? null, effectiveEnd);
const today = isToday(thisDate);
const marked = isSelected || isRangeStart || isRangeEnd;
let bg = "transparent";
if (marked) bg = "var(--flux-primary-400)";
else if (isInRange) bg = "var(--flux-primary-100)";
let textColor = "var(--flux-black)";
if (marked) textColor = "var(--flux-white)";
else if (today) textColor = "var(--flux-primary-400)";
let borderRadius: string;
if (marked || (!isInRange)) {
borderRadius = "var(--flux-radius-full)";
} else {
if (isRangeStart) borderRadius = "var(--flux-radius-full) 0 0 var(--flux-radius-full)";
else if (isRangeEnd) borderRadius = "0 var(--flux-radius-full) var(--flux-radius-full) 0";
else borderRadius = "0";
}
return (
<button
key={idx}
type="button"
onClick={() => onSelect(thisDate)}
onMouseEnter={() => onHover?.(thisDate)}
onMouseLeave={() => onHover?.(null)}
style={{
width: cellSize,
height: cellSize,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "0.8125rem",
fontWeight: marked ? 700 : today ? 600 : 400,
borderRadius,
background: bg,
color: textColor,
border: today && !marked
? "1.5px solid var(--flux-primary-200)"
: "1.5px solid transparent",
cursor: "pointer",
transition: "background 100ms var(--flux-ease), color 100ms var(--flux-ease)",
outline: "none",
fontFamily: "inherit",
}}
onFocus={(e) => {
e.currentTarget.style.boxShadow = "0 0 0 2px var(--flux-primary-300)";
}}
onBlur={(e) => {
e.currentTarget.style.boxShadow = "none";
}}
>
{day}
</button>
);
})}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// MonthNav
// ---------------------------------------------------------------------------
interface MonthNavProps {
year: number;
month: number;
onChange: (year: number, month: number) => void;
}
function MonthNav({ year, month, onChange }: MonthNavProps) {
const prev = () => {
if (month === 0) onChange(year - 1, 11);
else onChange(year, month - 1);
};
const next = () => {
if (month === 11) onChange(year + 1, 0);
else onChange(year, month + 1);
};
const navBtnStyle: React.CSSProperties = {
width: 26,
height: 26,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "var(--flux-radius-sm)",
border: "none",
background: "none",
cursor: "pointer",
color: "var(--flux-grey-400, #9ca3af)",
transition: "background 120ms, color 120ms",
fontFamily: "inherit",
};
return (
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: "12px",
}}
>
<button
type="button"
onClick={prev}
aria-label="Previous month"
style={navBtnStyle}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--flux-grey-50)";
e.currentTarget.style.color = "var(--flux-heading)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "none";
e.currentTarget.style.color = "var(--flux-grey-400, #9ca3af)";
}}
>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
<path
d="M8.5 10.5L4.5 6.5L8.5 2.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<span
style={{
fontSize: "0.875rem",
fontWeight: 700,
color: "var(--flux-heading)",
fontFamily: "inherit",
letterSpacing: "-0.01em",
}}
>
{MONTH_NAMES[month]} {year}
</span>
<button
type="button"
onClick={next}
aria-label="Next month"
style={navBtnStyle}
onMouseEnter={(e) => {
e.currentTarget.style.background = "var(--flux-grey-50)";
e.currentTarget.style.color = "var(--flux-heading)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.background = "none";
e.currentTarget.style.color = "var(--flux-grey-400, #9ca3af)";
}}
>
<svg width="13" height="13" viewBox="0 0 13 13" fill="none">
<path
d="M4.5 2.5L8.5 6.5L4.5 10.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>
);
}
// ---------------------------------------------------------------------------
// Section wrapper
// ---------------------------------------------------------------------------
function Section({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div className="mb-8">
<h3 className="text-sm font-bold text-[var(--flux-heading)] mb-3">{title}</h3>
<div className="p-6 rounded border border-[var(--flux-grey-100)] bg-[var(--flux-surface)]">
{children}
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// 1. Single Date Picker (Popover)
// ---------------------------------------------------------------------------
function SingleDatePicker() {
usePopoverKeyframes();
const today = new Date();
const [open, setOpen] = useState(false);
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth());
const [selected, setSelected] = useState<Date | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handler(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false);
}
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
const handleToggle = () => {
if (!open && selected) {
setYear(selected.getFullYear());
setMonth(selected.getMonth());
}
setOpen((v) => !v);
};
const handleSelect = (date: Date) => {
setSelected(date);
setOpen(false);
};
const chevronStyle: React.CSSProperties = {
color: "var(--flux-grey-300)",
flexShrink: 0,
transition: "transform 150ms",
transform: open ? "rotate(180deg)" : "rotate(0deg)",
};
return (
<div style={{ display: "flex", gap: "2.5rem", alignItems: "flex-end", flexWrap: "wrap" }}>
<div>
<label style={labelStyle}>Departure date</label>
<div ref={ref} style={{ position: "relative", display: "inline-block" }}>
<button
type="button"
onClick={handleToggle}
aria-haspopup="dialog"
aria-expanded={open}
style={triggerStyle(open)}
onMouseEnter={(e) => {
if (!open) e.currentTarget.style.borderColor = "var(--flux-grey-400, #9ca3af)";
}}
onMouseLeave={(e) => {
if (!open) e.currentTarget.style.borderColor = "var(--flux-grey-200)";
}}
>
<span
style={{
color: open ? "var(--flux-primary-400)" : "var(--flux-grey-300)",
display: "flex",
}}
>
<CalendarIcon />
</span>
<span
style={{
flex: 1,
textAlign: "left",
color: selected ? "var(--flux-black)" : "var(--flux-grey-300)",
}}
>
{selected ? formatInputValue(selected) : "DD / MM / YYYY"}
</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={chevronStyle}>
<path
d="M2 4L6 8L10 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{open && (
<div role="dialog" aria-label="Choose date" style={popoverStyle}>
<MonthNav
year={year}
month={month}
onChange={(y, m) => {
setYear(y);
setMonth(m);
}}
/>
<CalendarGrid
year={year}
month={month}
selected={selected}
onSelect={handleSelect}
/>
{selected && (
<div
style={{
marginTop: "10px",
paddingTop: "10px",
borderTop: "1px solid var(--flux-grey-100)",
display: "flex",
justifyContent: "flex-end",
}}
>
<button
type="button"
onClick={() => {
setSelected(null);
setOpen(false);
}}
style={clearLinkStyle}
>
Clear
</button>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// 2. Date Range Picker (Popover, single calendar)
// ---------------------------------------------------------------------------
function DateRangePicker() {
usePopoverKeyframes();
const today = new Date();
const [open, setOpen] = useState(false);
const [year, setYear] = useState(today.getFullYear());
const [month, setMonth] = useState(today.getMonth());
const [rangeStart, setRangeStart] = useState<Date | null>(null);
const [rangeEnd, setRangeEnd] = useState<Date | null>(null);
const [hovered, setHovered] = useState<Date | null>(null);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
function handler(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
setHovered(null);
}
}
document.addEventListener("mousedown", handler);
return () => document.removeEventListener("mousedown", handler);
}, []);
useEffect(() => {
function handler(e: KeyboardEvent) {
if (e.key === "Escape") {
setOpen(false);
setHovered(null);
}
}
document.addEventListener("keydown", handler);
return () => document.removeEventListener("keydown", handler);
}, []);
const handleToggle = () => {
setOpen((v) => !v);
setHovered(null);
};
const handleSelect = (date: Date) => {
if (!rangeStart || rangeEnd) {
setRangeStart(date);
setRangeEnd(null);
setHovered(null);
} else {
const t = date.getTime();
const s = rangeStart.getTime();
if (t < s) {
setRangeEnd(rangeStart);
setRangeStart(date);
} else {
setRangeEnd(date);
}
setHovered(null);
setOpen(false);
}
};
const handleClear = () => {
setRangeStart(null);
setRangeEnd(null);
setHovered(null);
};
const selectingEnd = rangeStart !== null && rangeEnd === null;
const hasSelection = rangeStart !== null;
const displayValue = formatRangeDisplay(rangeStart, rangeEnd);
const chevronStyle: React.CSSProperties = {
color: "var(--flux-grey-300)",
flexShrink: 0,
transition: "transform 150ms",
transform: open ? "rotate(180deg)" : "rotate(0deg)",
};
return (
<div style={{ display: "flex", gap: "2.5rem", alignItems: "flex-end", flexWrap: "wrap" }}>
<div>
<label style={labelStyle}>Date range</label>
<div ref={ref} style={{ position: "relative", display: "inline-block" }}>
<button
type="button"
onClick={handleToggle}
aria-haspopup="dialog"
aria-expanded={open}
style={triggerStyle(open, 240)}
onMouseEnter={(e) => {
if (!open) e.currentTarget.style.borderColor = "var(--flux-grey-400, #9ca3af)";
}}
onMouseLeave={(e) => {
if (!open) e.currentTarget.style.borderColor = "var(--flux-grey-200)";
}}
>
<span
style={{
color: open ? "var(--flux-primary-400)" : "var(--flux-grey-300)",
display: "flex",
}}
>
<CalendarIcon />
</span>
<span
style={{
flex: 1,
textAlign: "left",
color: hasSelection ? "var(--flux-black)" : "var(--flux-grey-300)",
fontWeight: hasSelection ? 500 : 400,
}}
>
{displayValue || "Select dates"}
</span>
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" style={chevronStyle}>
<path
d="M2 4L6 8L10 4"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
{open && (
<div role="dialog" aria-label="Choose date range" style={popoverStyle}>
<div
style={{
fontSize: "0.75rem",
fontWeight: 600,
color: "var(--flux-primary-400)",
marginBottom: "10px",
letterSpacing: "-0.01em",
visibility: selectingEnd ? "visible" : "hidden",
}}
>
Now select end date
</div>
<MonthNav
year={year}
month={month}
onChange={(y, m) => {
setYear(y);
setMonth(m);
}}
/>
<CalendarGrid
year={year}
month={month}
rangeStart={rangeStart}
rangeEnd={rangeEnd}
hovered={selectingEnd ? hovered : null}
onSelect={handleSelect}
onHover={selectingEnd ? setHovered : undefined}
/>
{hasSelection && (
<div
style={{
marginTop: "10px",
paddingTop: "10px",
borderTop: "1px solid var(--flux-grey-100)",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
{rangeStart && rangeEnd && (
<span
style={{
fontSize: "0.75rem",
color: "var(--flux-grey-400, #9ca3af)",
}}
>
{Math.round(
(rangeEnd.getTime() - rangeStart.getTime()) / 86_400_000
)}{" "}
nights
</span>
)}
<button
type="button"
onClick={handleClear}
style={{ ...clearLinkStyle, marginLeft: "auto" }}
>
Clear
</button>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
}
// ---------------------------------------------------------------------------
// Main demo export
// ---------------------------------------------------------------------------
export default function DatePickerDemo() {
return (
<div>
<Section title="1. Single Date Picker">
<p
style={{
fontSize: "0.875rem",
color: "var(--flux-grey-500)",
marginBottom: "20px",
}}
>
Input field that opens a calendar popover on click. Select a date to close
automatically. Dismiss with Escape or by clicking outside.
</p>
<SingleDatePicker />
</Section>
<Section title="2. Date Range Picker">
<p
style={{
fontSize: "0.875rem",
color: "var(--flux-grey-500)",
marginBottom: "20px",
}}
>
First click sets the start date, second click sets the end date and closes.
Range is highlighted as you hover. Shows as "May 27 – Jun 3".
</p>
<DateRangePicker />
</Section>
</div>
);
}