The deceptive simplicity
A date input looks easy. The user picks a date. Your code gets a value. Store it, compare it, display it.
In practice, "the user picks a date" is doing a lot of work in that sentence. What format do they enter? What format do they expect to see back? What timezone is the date in? Is it date-only or date-and-time? Date-and-time-and-zone? Just-the-date-but-the-time-component-matters-for-some-reason?
I've shipped at least one of each of the following bugs, and seen all of them in production code at other companies. Here's what each one looked like, and the design tradeoff for getting it right.
UI 1: The free-text "MM/DD/YYYY" input
What it looks like: A plain text input with placeholder MM/DD/YYYY and a JavaScript validator that parses with the obvious regex.
The bug: users in countries with day-first date conventions (most of Europe, much of Asia) instinctively type 15/05/2026 for May 15. The regex accepts it (since 15 ≤ 31), the date object parses it as the 15th month of 5 AD or rejects it depending on the parser, and the user gets a confusing error or a wrong date saved.
The fix: use the native HTML date input (<input type="date">), which sidesteps locale ambiguity by always using YYYY-MM-DD internally and displaying in the user's preferred format. Or, if you must use free text, autodetect the format from a non-ambiguous example and warn the user about your interpretation: "We read this as May 15, 2026. Is that right?"
UI 2: The picker that shows "today" highlighted, but today is wrong
What it looks like: A custom date picker built with a calendar grid. The "today" cell is highlighted based on JavaScript's new Date().
The bug: the user is in Tokyo. The server is in UTC. The page rendered server-side at 10 PM UTC, which is 7 AM the next day in Tokyo. The "today" highlight is on yesterday from the user's perspective.
This is especially confusing because the picker behavior is correct relative to its own internal logic but wrong relative to what the user sees on the wall clock.
The fix: always resolve "today" client-side, never server-side. Even if you render the picker server-side, leave the "today" determination to JavaScript that runs after page load.
UI 3: The picker that won't let you pick a date in the past — except for one day per year
What it looks like: An events form with a date picker that disables past dates. The validation runs both client-side (in JS) and server-side (in the API).
The bug: client-side checks selectedDate < new Date() using the user's local timezone. Server-side checks the same comparison but using server time. They disagree at the day boundary. A user in Australia opening the picker at 11:30 PM local time can select "tomorrow" (Australia time), submit the form, and the server (in UTC, which is still earlier in the day) sees the date as "today" and rejects it as not-in-the-future.
The fix: do all validation on the server in the user's timezone (require it to be supplied with the request), or accept any date the client supplied and apply business logic that's timezone-aware.
UI 4: The "5 minutes from now" reminder that fires immediately
What it looks like: A "remind me in 5 minutes" button that sets a notification time.
The bug: the code does const reminderTime = new Date(Date.now() + 5 * 60 * 1000); which is fine. Then it stores reminderTime.toISOString() which is also fine. Then on a different device, it parses that ISO string and compares against the local time. If the user's local clock is off (NTP drift, deliberate skew, virtual machine clock skew), the reminder fires immediately because "5 minutes from now" on Device A is "10 minutes ago" on Device B.
The fix: for "duration from now" intents, store the duration, not the absolute time. The reminder system computes the actual time each evaluation. This avoids any cross-device clock drift entirely.
UI 5: The date that displays differently after editing
What it looks like: A form pre-filled with an existing date. The user changes nothing. They click Save. The date in the database changes.
The bug: the date was stored as 2026-05-15T00:00:00.000Z (midnight UTC). The form displays it as 2026-05-15 in the user's timezone (let's say Sydney). When the user saves without changes, the form code reads back 2026-05-15 from the input, attaches the user's timezone, and serializes back to UTC. That's 2026-05-14T14:00:00.000Z — the previous day's mid-afternoon UTC. Save → silent date corruption.
The fix: store dates and times distinctly. A "date" is a calendar date, no timezone. A "moment" is an instant in time, with a timezone. Don't mix them. If your business object is "the day someone was born," store it as a date string (YYYY-MM-DD) with no time component at all. If your business object is "the moment someone was born" — which is what hospital records care about — store the full timestamp with timezone.
This is why PostgreSQL, MySQL, and most modern databases have both a DATE type (no time) and a TIMESTAMPTZ type (with time and timezone). Use the right one for the right concept.
The framework that gets it almost right
If you're starting a new project today, the best date primitives I've seen are:
- JavaScript Temporal API (Stage 3 as of 2026): proper distinct types for
PlainDate,PlainTime,ZonedDateTime, etc. It is finally polyfillable. - Python
datetimemodule with the convention "always passtzinfo" and usezoneinfo. - Rust's
chronocrate withDateTime<Tz>. - Go's
time.Time— has both moment and zone built in correctly.
The pattern: the type system enforces which kind of date you have. You can't accidentally compare a date to a timestamp because the compiler stops you.
Languages that lump everything into one "Date" or "DateTime" type (older JavaScript, PHP's DateTime with implicit local timezone, C's time_t) make these bugs nearly inevitable.
The four questions to ask before writing a date input
- Date or moment? "When were you born?" is a date. "When did you log in?" is a moment.
- If a moment, whose timezone matters? The user's? The system's? The business's? Pick one and document it.
- If a date, is it the user's local date or a global one? "Today's specials" depends on whose today. "Christmas Day" is global.
- What happens at edit time? Pre-fill, display, and read-back should all be exactly inverse. Test by editing without changing anything and verifying nothing actually changed.
For testing edge cases on a known timestamp, the main converter shows you every format simultaneously — useful when debugging a "what did this date actually mean?" mystery. For comparing two timestamps that should be the same but aren't, the difference calculator tells you exactly how far apart they are.
Published April 5, 2026. Tagged: ux, forms, design.