Date and time
Date range
A calendar that picks a start and an end day, with a live preview of the span and optional quick ranges.
- Built on
Intl.DateTimeFormatIntl.DateTimeFormat formatRange@starting-style- Client JavaScript
- Yes, for state
- Works without React
- React only (needs JavaScript)
- Source, gzipped
- 5.6 kB
- Built from
- Button
November 2026
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 |
Code
import { DateRange } from "@/components/equip/date-range";
export default function Demo() {
return (
<DateRange
size="s"
defaultValue={{ start: "2026-11-09", end: "2026-11-13" }}
today="2026-11-10"
locale="en-US"
weekStartsOn={0}
/>
);
}Examples
Two months with quick ranges
A preset button reads as pressed while the selection matches it exactly.
November 2026
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 |
| 23 | 24 | 25 | 26 | 27 | 28 | 29 |
| 30 |
December 2026
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Code
import { DateRange } from "@/components/equip/date-range";
export default function Example() {
return (
<DateRange
months={2}
today="2026-11-13"
defaultValue={{ start: "2026-11-27", end: "2026-11-30" }}
presets={[
{ label: "Next 7 days", start: "2026-11-14", end: "2026-11-20" },
{ label: "Black Friday weekend", start: "2026-11-27", end: "2026-11-30" },
{ label: "Holidays", start: "2026-12-21", end: "2027-01-03" },
]}
/>
);
}Booking window
The first pick sets the start; the span follows the pointer or focus until the second pick sets the end. Escape starts over.
December 2026
| M | T | W | T | F | S | S |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
4 nights at Harbour Lodge
Code
"use client";
import { useState } from "react";
import { DateRange, type DateRangeValue } from "@/components/equip/date-range";
const DAY = 86_400_000;
export default function Example() {
const [stay, setStay] = useState<DateRangeValue | null>({ start: "2026-12-18", end: "2026-12-22" });
const nights = stay ? (Date.parse(stay.end) - Date.parse(stay.start)) / DAY : 0;
return (
<div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
<DateRange value={stay} onValueChange={setStay} min="2026-11-13" max="2027-03-31" today="2026-11-13" />
<p style={{ margin: 0, color: "var(--eq-ink-2)", fontSize: "var(--eq-text-s)" }}>
{nights === 0 ? "Day visit" : `${nights} ${nights === 1 ? "night" : "nights"}`} at Harbour Lodge
</p>
</div>
);
}Props
| Prop | Type | Default | What it does |
|---|---|---|---|
value | { start: string; end: string } | null | none | ISO start and end days, inclusive. |
defaultValue | { start: string; end: string } | null | none | Starting range when uncontrolled. |
onValueChange | (value: { start: string; end: string }) => void | none | Called once both ends are picked, always with start on or before end. |
min | string | none | Earliest selectable day. Keyboard movement stops here. |
max | string | none | Latest selectable day. Keyboard movement stops here. |
months | 1 | 2 | 1 | Months shown side by side. They wrap under each other when narrow. |
presets | { label: string; start: string; end: string }[] | none | Quick ranges shown as small buttons under the grid. |
today | string | none | Today as an ISO date. When absent, the device date is read after mount so the server render stays stable. |
locale | string | "en-GB" | Locale for month, weekday and day names. |
weekStartsOn | 0 | 1 | 1 | 0 starts weeks on Sunday, 1 on Monday. |
size | "s" | "m" | "m" | Day cells of 24px or 32px. |
...rest | ComponentProps<"div"> | none | Every native div attribute, including ref. |
Anatomy
| root | .eq-date-range | Has data-choosing while the end is being picked. |
|---|---|---|
| months | .eq-date-range-months | |
| month | .eq-date-range-month | |
| header | .eq-date-range-header | |
| title | .eq-date-range-title | Month and year, a polite live region. |
| nav | .eq-date-range-nav | Previous month on the first month, next month on the last. |
| grid | .eq-date-range-grid | One table with role="grid" per month. |
| weekday | .eq-date-range-weekday | |
| day | .eq-date-range-day | A gridcell. Its ::before is the span bar; data-edge marks the start and end. |
| mark | .eq-date-range-mark | The number; solid on the two ends. |
| blank | .eq-date-range-blank | Empty cell where a day belongs to another month. |
| presets | .eq-date-range-presets | Quick range Buttons, pressed when the range matches. |
| status | .eq-date-range-status | Visually hidden live text with the chosen range. |
Keyboard
| ArrowLeftArrowRight | Moves to the previous or next day, across both months. |
|---|---|
| ArrowUpArrowDown | Moves to the same day in the previous or next week. |
| HomeEnd | Moves to the first or last day of the week. |
| PageUpPageDown | Moves to the same day in the previous or next month. |
| Shift+PageUpShift+PageDown | Moves to the same day in the previous or next year. |
| EnterSpace | Sets the start, then the end. While choosing the end, focus previews the span. |
| Escape | Drops a half-chosen range and keeps the previous one. |
Notes
- Follows the WAI-ARIA date grid pattern with one focusable day; each day's name is its full date.
- Days in the range carry aria-selected, and a hidden live region announces the start, then the finished range.
- The value only changes when both ends are picked; picking the end before the start swaps them.
- Picking the same day twice gives a one-day range.
- Days from the months around are left blank so the bar reads cleanly and no date appears twice with two months shown.
- Today carries a dot under the number as well as aria-current, so it does not depend on colour.
- The month button labels and the announced status are fixed English text; the dates follow the locale prop.
Source
Two files, and they are the whole part. Open either to read it, copy it straight from its bar, or add the part to your loadout and install it with everything else you picked.
components/equip/date-range.tsx413 lines
"use client";
import {
useEffect,
useId,
useMemo,
useRef,
useState,
type ComponentProps,
type KeyboardEvent,
} from "react";
import { Button } from "./button";
import "./date-range.css";
export type DateRangeValue = { start: string; end: string };
export type DateRangeProps = Omit<ComponentProps<"div">, "defaultValue" | "onChange"> & {
/** ISO start and end days, inclusive. */
value?: DateRangeValue | null;
defaultValue?: DateRangeValue | null;
onValueChange?: (value: DateRangeValue) => void;
min?: string;
max?: string;
/** Months shown side by side. */
months?: 1 | 2;
/** Quick ranges shown as buttons under the grid. */
presets?: { label: string; start: string; end: string }[];
/** Today as an ISO date. When absent it is read from the device after mount. */
today?: string;
locale?: string;
weekStartsOn?: 0 | 1;
size?: "s" | "m";
};
const pad = (n: number, length = 2) => String(n).padStart(length, "0");
function toDate(iso: string) {
const [y, m, d = 1] = iso.split("-").map(Number);
const date = new Date(0);
date.setUTCFullYear(y, m - 1, d);
return date;
}
function toISO(date: Date) {
return `${pad(date.getUTCFullYear(), 4)}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}`;
}
function daysIn(year: number, month: number) {
const date = new Date(0);
date.setUTCFullYear(year, month, 0);
return date.getUTCDate();
}
function addDays(iso: string, n: number) {
const date = toDate(iso);
date.setUTCDate(date.getUTCDate() + n);
return toISO(date);
}
function addMonths(iso: string, n: number) {
const [y, m, d] = iso.split("-").map(Number);
const total = y * 12 + m - 1 + n;
const year = Math.floor(total / 12);
const month = (total % 12) + 1;
return `${pad(year, 4)}-${pad(month)}-${pad(Math.min(d, daysIn(year, month)))}`;
}
const addMonth = (month: string, n: number) => addMonths(`${month}-01`, n).slice(0, 7);
const column = (iso: string, weekStartsOn: number) => (toDate(iso).getUTCDay() - weekStartsOn + 7) % 7;
const ordered = (a: string, b: string): DateRangeValue => (a <= b ? { start: a, end: b } : { start: b, end: a });
function localToday() {
const now = new Date();
return `${pad(now.getFullYear(), 4)}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
}
/** Weeks of the month, with null where a day belongs to the month before or after. */
function weeksOf(month: string, weekStartsOn: number) {
const [y, m] = month.split("-").map(Number);
const cells: (string | null)[] = Array(column(`${month}-01`, weekStartsOn)).fill(null);
for (let d = 1; d <= daysIn(y, m); d++) cells.push(`${month}-${pad(d)}`);
while (cells.length % 7) cells.push(null);
return Array.from({ length: cells.length / 7 }, (_, w) => cells.slice(w * 7, w * 7 + 7));
}
export function DateRange({
value,
defaultValue = null,
onValueChange,
min,
max,
months = 1,
presets,
today: todayProp,
locale = "en-GB",
weekStartsOn = 1,
size = "m",
className,
...rest
}: DateRangeProps) {
const id = useId();
const [rangeState, setRangeState] = useState(defaultValue);
const range = value !== undefined ? value : rangeState;
const [deviceToday, setDeviceToday] = useState<string | null>(null);
const today = todayProp ?? deviceToday;
const [first, setFirst] = useState<string | null>(() => (range?.start ?? todayProp ?? min)?.slice(0, 7) ?? null);
const [focused, setFocused] = useState<string | null>(null);
const [anchor, setAnchor] = useState<string | null>(null);
const [hovered, setHovered] = useState<string | null>(null);
const [focusInside, setFocusInside] = useState(false);
const [status, setStatus] = useState("");
const focusPending = useRef(false);
const [lastFirst, setLastFirst] = useState(first);
const [slide, setSlide] = useState<"next" | "prev" | undefined>();
if (first !== lastFirst) {
setLastFirst(first);
setSlide(lastFirst && first ? (first < lastFirst ? "prev" : "next") : undefined);
}
useEffect(() => {
if (todayProp) return;
const now = localToday();
setDeviceToday(now);
setFirst((current) => current ?? now.slice(0, 7));
}, [todayProp]);
const format = useMemo(
() => ({
title: new Intl.DateTimeFormat(locale, { month: "long", year: "numeric", timeZone: "UTC" }),
day: new Intl.DateTimeFormat(locale, { day: "numeric", timeZone: "UTC" }),
full: new Intl.DateTimeFormat(locale, { dateStyle: "full", timeZone: "UTC" }),
span: new Intl.DateTimeFormat(locale, { dateStyle: "medium", timeZone: "UTC" }),
weekday: new Intl.DateTimeFormat(locale, { weekday: "narrow", timeZone: "UTC" }),
weekdayLong: new Intl.DateTimeFormat(locale, { weekday: "long", timeZone: "UTC" }),
}),
[locale],
);
const shownMonths = first ? Array.from({ length: months }, (_, i) => addMonth(first, i)) : [];
const last = shownMonths[shownMonths.length - 1];
const isShown = (iso: string) => shownMonths.includes(iso.slice(0, 7));
const disabled = (iso: string) => (min !== undefined && iso < min) || (max !== undefined && iso > max);
const clamp = (iso: string) => (min !== undefined && iso < min ? min : max !== undefined && iso > max ? max : iso);
function fallback() {
if (!first) return null;
for (const candidate of [anchor, range?.start, range?.end, today]) {
if (candidate && isShown(candidate)) return candidate;
}
const start = `${first}-01`;
return min !== undefined && min.startsWith(first) && start < min ? min : start;
}
const active = focused && isShown(focused) ? focused : fallback();
const tip = anchor ? (hovered ?? (focusInside ? active : null)) : null;
const span = anchor ? ordered(anchor, tip ?? anchor) : range;
function showMonthOf(iso: string) {
const month = iso.slice(0, 7);
if (!first || isShown(iso)) return;
setFirst(month < first ? month : addMonth(month, 1 - months));
}
function move(target: string) {
const next = clamp(target);
setHovered(null);
if (next === active) return;
focusPending.current = true;
setFocused(next);
showMonthOf(next);
}
function step(n: number) {
if (!first) return;
setFirst(addMonth(first, n));
if (active) setFocused(clamp(addMonths(active, n)));
}
function commit(next: DateRangeValue) {
if (value === undefined) setRangeState(next);
onValueChange?.(next);
}
function pick(iso: string) {
if (disabled(iso)) return;
setFocused(iso);
if (anchor) {
setAnchor(null);
commit(ordered(anchor, iso));
setStatus(spanText(ordered(anchor, iso)));
} else {
setAnchor(iso);
setStatus(`Start ${format.full.format(toDate(iso))}. Choose the end date.`);
}
}
const spanText = (r: DateRangeValue) => format.span.formatRange(toDate(r.start), toDate(r.end));
function applyPreset(start: string, end: string) {
setAnchor(null);
commit(ordered(start, end));
setStatus(spanText(ordered(start, end)));
setFocused(null);
setFirst(start.slice(0, 7));
}
useEffect(() => {
if (!focusPending.current || !active) return;
focusPending.current = false;
const target = document.getElementById(`${id}-${active}`);
const current = document.activeElement;
if (!target || (current && current !== document.body && !target.closest(".eq-date-range")?.contains(current))) return;
target.focus();
}, [active, id]);
function handleKeyDown(event: KeyboardEvent<HTMLDivElement>) {
if (!active || !(event.target instanceof HTMLElement) || !event.target.classList.contains("eq-date-range-day")) return;
let next: string;
switch (event.key) {
case "ArrowLeft":
next = addDays(active, -1);
break;
case "ArrowRight":
next = addDays(active, 1);
break;
case "ArrowUp":
next = addDays(active, -7);
break;
case "ArrowDown":
next = addDays(active, 7);
break;
case "Home":
next = addDays(active, -column(active, weekStartsOn));
break;
case "End":
next = addDays(active, 6 - column(active, weekStartsOn));
break;
case "PageUp":
next = addMonths(active, event.shiftKey ? -12 : -1);
break;
case "PageDown":
next = addMonths(active, event.shiftKey ? 12 : 1);
break;
case "Enter":
case " ":
event.preventDefault();
pick(active);
return;
case "Escape":
if (!anchor) return;
event.preventDefault();
event.stopPropagation();
setAnchor(null);
setStatus("");
return;
default:
return;
}
event.preventDefault();
move(next);
}
const prevOff = !first || (min !== undefined && addMonth(first, -1) < min.slice(0, 7));
const nextOff = !last || (max !== undefined && addMonth(last, 1) > max.slice(0, 7));
const weekdays = Array.from({ length: 7 }, (_, i) => toDate(addDays("2026-11-01", weekStartsOn + i)));
const chevron = (d: string) => (
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d={d} stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
return (
<div
{...rest}
className={["eq-date-range", className].filter(Boolean).join(" ")}
data-size={size}
data-choosing={anchor ? "" : undefined}
>
<div
className="eq-date-range-months"
onKeyDown={handleKeyDown}
onFocus={() => setFocusInside(true)}
onBlur={(event) => {
if (!event.currentTarget.contains(event.relatedTarget)) setFocusInside(false);
}}
>
{shownMonths.map((month, index) => {
const titleId = `${id}-title-${index}`;
const [y, m] = month.split("-").map(Number);
const monthDays = daysIn(y, m);
return (
<div key={index} className="eq-date-range-month">
<div className="eq-date-range-header">
{index === 0 && (
<button
type="button"
className="eq-date-range-nav"
aria-label="Previous month"
disabled={prevOff}
onClick={() => step(-1)}
>
{chevron("M10 3.5 5.5 8l4.5 4.5")}
</button>
)}
<span className="eq-date-range-title" id={titleId} aria-live="polite">
{format.title.format(toDate(`${month}-01`))}
</span>
{index === shownMonths.length - 1 && (
<button
type="button"
className="eq-date-range-nav"
aria-label="Next month"
disabled={nextOff}
onClick={() => step(1)}
>
{chevron("m6 3.5 4.5 4.5L6 12.5")}
</button>
)}
</div>
<table
key={month}
className="eq-date-range-grid"
data-slide={slide}
role="grid"
aria-labelledby={titleId}
aria-multiselectable="true"
onPointerLeave={() => setHovered(null)}
>
<thead>
<tr>
{weekdays.map((date) => (
<th
key={date.getUTCDay()}
className="eq-date-range-weekday"
scope="col"
abbr={format.weekdayLong.format(date)}
>
{format.weekday.format(date)}
</th>
))}
</tr>
</thead>
<tbody>
{weeksOf(month, weekStartsOn).map((week, w) => (
<tr key={w}>
{week.map((iso, col) => {
if (!iso) return <td key={col} className="eq-date-range-blank" role="gridcell" />;
const inSpan = span !== null && iso >= span.start && iso <= span.end;
const day = Number(iso.slice(8));
const edge =
span && iso === span.start && iso === span.end
? "both"
: span && iso === span.start
? "start"
: span && iso === span.end
? "end"
: undefined;
return (
<td
key={iso}
id={`${id}-${iso}`}
className="eq-date-range-day"
role="gridcell"
tabIndex={iso === active ? 0 : -1}
aria-label={format.full.format(toDate(iso))}
aria-selected={(!anchor && inSpan) || iso === anchor}
aria-disabled={disabled(iso) || undefined}
aria-current={iso === today ? "date" : undefined}
data-in-span={inSpan || undefined}
data-edge={inSpan ? edge : undefined}
data-cap-start={col === 0 || day === 1 || undefined}
data-cap-end={col === 6 || day === monthDays || undefined}
onClick={() => pick(iso)}
onPointerEnter={() => setHovered(iso)}
>
<span className="eq-date-range-mark">{format.day.format(toDate(iso))}</span>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
})}
</div>
{presets && presets.length > 0 && (
<div className="eq-date-range-presets" role="group" aria-label="Quick ranges">
{presets.map((preset) => {
const on = !anchor && range?.start === preset.start && range?.end === preset.end;
return (
<Button
key={preset.label}
size="s"
variant={on ? "solid" : "outline"}
aria-pressed={on}
onClick={() => applyPreset(preset.start, preset.end)}
>
{preset.label}
</Button>
);
})}
</div>
)}
<span className="eq-date-range-status" aria-live="polite">
{status}
</span>
</div>
);
}components/equip/date-range.css290 lines
@layer equip {
.eq-date-range {
--_cell: 32px;
--_nav: var(--eq-h-s);
--_band: var(--eq-tint-strong);
position: relative;
display: inline-grid;
gap: var(--eq-space-3);
color: var(--eq-ink);
font: var(--eq-weight) var(--eq-text-m) / 1 var(--eq-font);
font-variant-numeric: tabular-nums;
}
.eq-date-range[data-size="s"] {
--_cell: 24px;
--_nav: 24px;
gap: var(--eq-space-2);
font-size: var(--eq-text-xs);
}
/* While the end is being chosen the span is only a preview, so it is lighter. */
.eq-date-range[data-choosing] {
--_band: var(--eq-tint);
}
.eq-date-range-months {
display: flex;
flex-wrap: wrap;
align-items: start;
gap: var(--eq-space-5);
}
.eq-date-range-month {
display: grid;
gap: var(--eq-space-2);
}
.eq-date-range[data-size="s"] .eq-date-range-month {
gap: var(--eq-space-1);
}
.eq-date-range-header {
display: flex;
align-items: center;
gap: var(--eq-space-1);
min-height: var(--_nav);
}
.eq-date-range-title {
flex: 1;
text-align: center;
font-size: var(--eq-text-m);
font-weight: var(--eq-weight-strong);
letter-spacing: -0.005em;
}
.eq-date-range[data-size="s"] .eq-date-range-title {
font-size: var(--eq-text-s);
}
.eq-date-range-nav {
display: inline-grid;
place-items: center;
flex: none;
width: var(--_nav);
height: var(--_nav);
padding: 0;
border: 0;
border-radius: var(--eq-radius-s);
background: transparent;
color: var(--eq-ink-2);
cursor: pointer;
transition:
background-color var(--eq-dur) var(--eq-ease),
color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
.eq-date-range-nav > svg {
width: 16px;
height: 16px;
}
.eq-date-range-nav:active:not(:disabled) {
scale: var(--eq-press);
}
.eq-date-range-nav:focus-visible {
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-date-range-nav:disabled {
opacity: 0.45;
cursor: not-allowed;
}
.eq-date-range-grid {
border-collapse: separate;
border-spacing: 0;
table-layout: fixed;
transition:
translate var(--eq-dur-move) var(--eq-spring),
opacity var(--eq-dur) var(--eq-ease);
}
@starting-style {
.eq-date-range-grid[data-slide="next"] {
translate: 6px 0;
opacity: 0;
}
.eq-date-range-grid[data-slide="prev"] {
translate: -6px 0;
opacity: 0;
}
}
.eq-date-range-weekday {
width: var(--_cell);
height: calc(var(--_cell) * 0.75);
padding: 0;
color: var(--eq-ink-3);
font-size: var(--eq-text-xs);
font-weight: var(--eq-weight);
text-align: center;
vertical-align: middle;
}
.eq-date-range-blank {
width: var(--_cell);
height: var(--_cell);
padding: 0;
}
.eq-date-range-day {
position: relative;
isolation: isolate;
width: var(--_cell);
height: var(--_cell);
padding: 0;
text-align: center;
vertical-align: middle;
cursor: pointer;
user-select: none;
outline: none;
}
/* The band: one continuous bar across the span, square where days meet. */
.eq-date-range-day::before {
content: "";
position: absolute;
inset: 2px 0;
z-index: -1;
background: transparent;
transition: background-color var(--eq-dur) var(--eq-ease);
}
.eq-date-range-day[data-in-span]::before {
background: var(--_band);
}
.eq-date-range-day[data-edge="start"]::before {
left: 50%;
}
.eq-date-range-day[data-edge="end"]::before {
right: 50%;
}
.eq-date-range-day[data-edge="both"]::before {
background: transparent;
}
.eq-date-range-day[data-in-span][data-cap-start]:not([data-edge])::before {
left: 2px;
border-start-start-radius: var(--eq-radius-s);
border-end-start-radius: var(--eq-radius-s);
}
.eq-date-range-day[data-in-span][data-cap-end]:not([data-edge])::before {
right: 2px;
border-start-end-radius: var(--eq-radius-s);
border-end-end-radius: var(--eq-radius-s);
}
.eq-date-range-mark {
position: relative;
isolation: isolate;
display: grid;
place-items: center;
width: calc(var(--_cell) - 2px);
height: calc(var(--_cell) - 2px);
margin: auto;
border-radius: var(--eq-radius-s);
transition:
background-color var(--eq-dur) var(--eq-ease),
color var(--eq-dur) var(--eq-ease),
scale var(--eq-dur) var(--eq-ease);
}
/* The end marks grow into place. */
.eq-date-range-mark::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
background: var(--eq-solid);
opacity: 0;
scale: 0.6;
transition:
opacity var(--eq-dur) var(--eq-ease),
scale var(--eq-dur-move) var(--eq-spring);
}
.eq-date-range-day[data-edge] .eq-date-range-mark {
color: var(--eq-solid-ink);
font-weight: var(--eq-weight-strong);
}
.eq-date-range-day[data-edge] .eq-date-range-mark::before {
opacity: 1;
scale: 1;
}
/* Today: a dot under the number, so it does not rely on colour. */
.eq-date-range-day[aria-current="date"] .eq-date-range-mark::after {
content: "";
position: absolute;
left: 50%;
bottom: 2px;
width: 4px;
height: 4px;
margin-left: -2px;
border-radius: var(--eq-radius-full);
background: currentColor;
}
.eq-date-range[data-size="s"] .eq-date-range-day[aria-current="date"] .eq-date-range-mark::after {
bottom: 1px;
width: 3px;
height: 3px;
margin-left: -1.5px;
}
@media (hover: hover) {
.eq-date-range-nav:hover:not(:disabled) {
background: var(--eq-tint);
color: var(--eq-ink);
}
.eq-date-range-day:hover:not([data-edge], [aria-disabled="true"]) .eq-date-range-mark {
background: var(--eq-tint);
}
}
.eq-date-range-day:active:not([aria-disabled="true"]) .eq-date-range-mark {
scale: var(--eq-press);
}
.eq-date-range-day:focus-visible .eq-date-range-mark {
z-index: 1;
outline: 2px solid var(--eq-focus);
outline-offset: 2px;
}
.eq-date-range-day[aria-disabled="true"] {
opacity: 0.45;
cursor: not-allowed;
text-decoration: line-through;
}
.eq-date-range-presets {
display: flex;
flex-wrap: wrap;
gap: var(--eq-space-2);
}
.eq-date-range-status {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
}