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
SMTWTFS
1234567
891011121314
15161718192021
22232425262728
2930
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
MTWTFSS
1
2345678
9101112131415
16171819202122
23242526272829
30
December 2026
MTWTFSS
123456
78910111213
14151617181920
21222324252627
28293031
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
MTWTFSS
123456
78910111213
14151617181920
21222324252627
28293031

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

PropTypeDefaultWhat it does
value{ start: string; end: string } | nullnoneISO start and end days, inclusive.
defaultValue{ start: string; end: string } | nullnoneStarting range when uncontrolled.
onValueChange(value: { start: string; end: string }) => voidnoneCalled once both ends are picked, always with start on or before end.
minstringnoneEarliest selectable day. Keyboard movement stops here.
maxstringnoneLatest selectable day. Keyboard movement stops here.
months1 | 21Months shown side by side. They wrap under each other when narrow.
presets{ label: string; start: string; end: string }[]noneQuick ranges shown as small buttons under the grid.
todaystringnoneToday as an ISO date. When absent, the device date is read after mount so the server render stays stable.
localestring"en-GB"Locale for month, weekday and day names.
weekStartsOn0 | 110 starts weeks on Sunday, 1 on Monday.
size"s" | "m""m"Day cells of 24px or 32px.
...restComponentProps<"div">noneEvery native div attribute, including ref.

Anatomy

root.eq-date-rangeHas 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-titleMonth and year, a polite live region.
nav.eq-date-range-navPrevious month on the first month, next month on the last.
grid.eq-date-range-gridOne table with role="grid" per month.
weekday.eq-date-range-weekday
day.eq-date-range-dayA gridcell. Its ::before is the span bar; data-edge marks the start and end.
mark.eq-date-range-markThe number; solid on the two ends.
blank.eq-date-range-blankEmpty cell where a day belongs to another month.
presets.eq-date-range-presetsQuick range Buttons, pressed when the range matches.
status.eq-date-range-statusVisually hidden live text with the chosen range.

Keyboard

ArrowLeftArrowRightMoves to the previous or next day, across both months.
ArrowUpArrowDownMoves to the same day in the previous or next week.
HomeEndMoves to the first or last day of the week.
PageUpPageDownMoves to the same day in the previous or next month.
Shift+PageUpShift+PageDownMoves to the same day in the previous or next year.
EnterSpaceSets the start, then the end. While choosing the end, focus previews the span.
EscapeDrops 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;
  }
}

EqUIp is in beta: free for non-commercial use, no warranty, and things can change. Terms

Nothing picked yet. Press A on a part, or use the plus on the board, and the install command builds here.

Loading the index.

Loading the index