AniUI Academy

Build a date range picker

mediumState & DataHigh freq+200 XP

Build a date range picker — a start date input and an end date input.

Both are controlled text inputs that accept an ISO-shaped date (YYYY-MM-DD).

The end date must not be before the start date; when the user tries, surface a visible error and don't update the committed range.

Examples

// Both inputs empty → summary = "" , no error.

// start = "2026-01-10", end = "2026-01-20"
// → summary reads "2026-01-10 to 2026-01-20"

// Change end to "2026-01-05"
// → summary clears, error appears:
//   "End date cannot be before start date"

// Fix end to "2026-02-01"
// → error clears, summary updates.

The committed range and the error are mutually exclusive — never both at once.

const hasBoth = start !== "" && end !== "";
const isInvalid = hasBoth && end < start;
const summary = hasBoth && !isInvalid ? `${start} to ${end}` : "";
const error = isInvalid ? "End date cannot be before start date" : "";

ISO YYYY-MM-DD strings compare correctly as plain strings — no `Date` parsing needed.

What it has to do

  • The start input is `<input data-testid="start-date" />`.
  • The end input is `<input data-testid="end-date" />`.
  • An element `<p data-testid="range-summary">` displays the committed range as `"<start> to <end>"` when both are set, or an empty string when either is missing.
  • If the user types an end date earlier than the start, show `<p data-testid="range-error">End date cannot be before start date</p>` and leave the committed range unchanged.
  • Clearing one or both inputs clears the error and the summary accordingly — never show both an error and a committed range at the same time.

Notes

  • Derive both `summary` and `error` from the inputs; don't track them as separate state. Separate state means two places can disagree.
  • Validate on each change rather than on blur — users expect the error to appear and clear as they type, not surprise them later.
  • Timezone-safe comparison matters once you move past string compare: compare day boundaries, not full `Date` instants, or a user west of UTC can accidentally cross midnight on "end === start".
  • An accessible error should sit in a region announced to assistive tech (`aria-live="polite"` on a wrapper), and the error text should be tied to the end input with `aria-describedby`.
Asked atAirbnbUberStripeShopifyFlipkart

Stuck? The react course covers the ideas this problem is built on.

import { useState } from "react";

export default function App() {
  const [start, setStart] = useState("");
  const [end, setEnd] = useState("");

  // TODO: compute isValid, summary, error from start/end.
  // Remember: never show both a committed range and an error at the same time.
  const summary = "";
  const error = "";

  return (
    <div style={{ fontFamily: "sans-serif", padding: 16, display: "grid", gap: 8, maxWidth: 320 }}>
      <label>
        Start:{" "}
        <input
          type="date"
          data-testid="start-date"
          value={start}
          onChange={(e) => setStart(e.target.value)}
        />
      </label>
      <label>
        End:{" "}
        <input
          type="date"
          data-testid="end-date"
          value={end}
          onChange={(e) => setEnd(e.target.value)}
        />
      </label>
      <p data-testid="range-summary">{summary}</p>
      {error ? <p data-testid="range-error" style={{ color: "crimson" }}>{error}</p> : null}
    </div>
  );
}

5 tests · +200 XP

Discussion

Sign in to post
Sign in to join the conversation.Sign in

No comments yet. Be the first to say something.