PostHog / PostHog/posthog

Date picker: LemonUI vs Quill feature parity audit

Open
#65,019 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
39.9k
Forks
3.4k
Avg merge
7h 27m
Merged PRs (30d)
222

Description

Summary

We're moving the main app toward the Quill design system. Quill has a DateTimePicker, while LemonUI has a whole family of calendar components in active use. Before adopting Quill's picker broadly we need a clear inventory of what the LemonUI family supports that Quill does not, and a per-feature decision: port it to Quill or intentionally drop it.

This issue is that inventory + a decision checklist.

Status (updated 2026-06-26)

The migration is underway via a seam: a PostHog-owned wrapper at frontend/src/lib/components/DatePicker/DatePicker.tsx that callers depend on. It speaks the existing dayjs-facing API and owns the trigger; its internals swap from LemonUI to Quill in one place. The swap is gated by the quill-date-picker feature flag (off by default; currently enabled for internal dogfooders), so LemonUI and Quill run side by side and roll back without a revert.

A quillCanRender capability gate falls the seam back to LemonUI whenever a request needs something the Quill panel can't do yet — so the flag can be enabled incrementally while gaps are closed.

Shipped so far:

  • #65056 — decoupled the LemonCalendar per-cell hooks (getLemonButtonProps/getLemonButtonTimeProps) into typed getDateState/getTimeState, removing the leaked LemonButton API.
  • #65059 — the DatePicker seam (LemonUI inside), with 2 pilot callers migrated (batch-export end date, visual-review quarantine expiry).
  • #65098 — a Quill single-date DatePicker (sibling of DateTimePicker, native Date), sharing a calendar-grid + segmented-date-input with the range picker.
  • #65102 / #65553 — jest+RTL for quill-components and wiring the quill packages into the CI jest run.
  • #65519 — flip the seam to Quill behind quill-date-picker, with a maxDate bound for future-date callers.
  • #65522 / #65531 / #65910 — Quill include-time toggle decoupled from initial time (showTimeToggle/onIncludeTimeChange); seam granularity + time-toggle mapping; trigger label tracks the toggle.
  • #65723 — retired the unused selectionPeriodLimit prop.
Caller migrations (each flag-gated, non-flagged behaviour unchanged)
  • #65994 — evaluation report schedule "Starts at" anchor (with a future maxDate).
  • #65995 — annotation modal date marker (with a one-year-ahead maxDate).

LemonUI-only features → port or drop?

  • Single-date selectionPORTED (#65098). Quill DatePicker is a single-date sibling of DateTimePicker (Date in/out), not a mode prop.
  • Time granularity (day / hour / minute)PORTED for day + minute (seam mapping in #65531: day -> no time, minute -> time on). ⚠️ hour-only still falls back to LemonUI — Quill has no hour-only mode.
  • "Include time" toggle (showTimeToggle / onToggleTime)PORTED (#65522 added showTimeToggle/onIncludeTimeChange to Quill; #65531 maps the seam; #65910 keeps the trigger label in sync with the toggle).
  • 12/24-hour format (use24HourFormat)GAP / not yet. Quill renders 24-hour numeric inputs only; the seam falls back to LemonUI when a caller explicitly needs 12-hour. Tracked panel-gap follow-up.
  • Restrict to past/upcoming + timezone (selectionPeriod*)DECIDED: use min/maxDate. The selectionPeriod / selectionPeriodLimit pair is a confusing way to express bounds; min/maxDate (Quill-native, clear) is the API going forward. selectionPeriodLimit had zero callers and was retired (#65723); maxDate was added to the seam (#65519). Still to wire: mapping selectionPeriod='past'/'upcoming' (+ timezone) onto Quill bounds so those callers stop falling back to LemonUI.
  • Clearable (clearable)PORTED. The seam owns the clear affordance on the trigger.
  • Placeholder (placeholder)PORTED. The seam owns the placeholder on the trigger.
  • Custom per-date button props (getLemonButtonProps)REDESIGNED (#65056). Replaced with typed getDateState / getTimeState that mirror Quill's isStart/isEnd/isBetween vocabulary and own the styling internally — no leaked LemonButton API.
  • Display format string (format)PORTED at the seam. The seam honours a dayjs format string for the trigger label; the Quill panel uses dateFormat segment order (MDY/DMY/YMD) for its inputs.
  • Controlled visibility + built-in trigger (visible, buttonProps)RESOLVED by architecture. Quill keeps the panel trigger-less; the PostHog seam owns the trigger + popover and exposes a visible / onOpen / onClickOutside / onClose trio for callers that drive visibility themselves.

Deferred Quill-path bugs (found in dogfooding + review)

Correctness/polish bugs in the Quill render path (flag on), surfaced while migrating callers and dogfooding. They live in the seam / Quill trigger, so they affect every migrated caller — not one scene. Deferred to a focused seam-polish pass rather than blocking the per-caller migration PRs (each caller is approved + flag-gated, non-flagged behaviour unchanged). These gate the wider rollout (see Remaining work).

  • 🔴 Timezone not honoured on the Quill path. The seam's Quill branch round-trips through a browser-local native Date (value.toDate() then onChange(dayjs(next)) in DatePicker.tsx), dropping the project timezone. Callers that build their value in project tz (e.g. the annotation modal: dayjs().tz(timezone), submits .toISOString()) persist an instant shifted by the browser/project offset when the flag is on. The LemonUI fallback is correct. Highest priority — silent wrong-instant write. Found reviewing #65995.
  • Trigger date format not localized. The Quill trigger label uses a hardcoded MMMM D, YYYY HH:mm / MMMM D, YYYY format regardless of locale. Found reviewing #65995.
  • Trigger tooltip stays open. The Quill trigger shows a tooltip that stays open persistently rather than on hover. Found reviewing #65995.
  • Trigger looks out of place in mixed forms. The Quill outline-button trigger is visually underweight next to neighbouring LemonUI inputs (e.g. a LemonSelect in the same form row). During rollout every migrated field sits in a half-Lemon/half-Quill form. Decision: style the seam's Quill trigger to match LemonUI inputs (border/height/radius/bg) so only the panel changes when the flag flips, vs accept the transitional look. Raised on #65995 + earlier dogfooding.
  • maxDate bounds only the Quill arm. Per the seam doc, the LemonUI fallback ignores maxDate and stays unbounded above, so a future-date bound is effectively flag-gated until LemonUI is retired. Documented limitation, not a regression — don't treat the bound as an enforced invariant elsewhere.
  • Nullable onChange into non-nullable caller fields. DatePicker's onChange: (value | null) can flow null into callers whose form field is non-nullable and whose submit path is unguarded (safe today only because no migrated caller passes clearable). A latent trap when clearable is later enabled.

Open question — RESOLVED

The "experimental" label on Quill's DateTimePicker was only a Storybook story name (THIS_COMPONENT_IS_EXPERIMENTAL), not a runtime gate. The confirmed migration target is Quill DatePicker (the new single-date sibling) for single dates and DateTimePicker for ranges.

Remaining work

  • Migrate the remaining ~12 single-date call sites off LemonCalendarSelectInput onto the seam (they pick up Quill for free behind the flag).
  • Close the remaining Quill panel gaps so fewer requests fall back: 12-hour entry, hour-only granularity, multi-month, and the selectionPeriod -> min/maxDate mapping.
  • Fix the deferred Quill-path bugs above (timezone, localized format, stuck tooltip, trigger styling) — these gate the rollout.
  • Build a DateRangePicker seam for the 3 range call sites (all in frontend/src/lib/components/DateFilter/DateFilter.tsx).
  • Roll the quill-date-picker flag out beyond internal dogfooders once parity holds and the deferred Quill-path bugs are fixed.
  • Delete the LemonCalendar* family once nothing imports it.

Original inventory + feature comparison (reference)

Sources

LemonUI:

  • frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendar.tsxLemonCalendarProps (grid primitive)
  • frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsxLemonCalendarSelectProps + LemonCalendarSelectInputProps (single date)
  • frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsxLemonCalendarRangeProps (range)
  • frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx — inline range

Quill (@posthog/quill-components):

  • packages/quill/packages/components/src/date-time-picker.tsxDateTimePickerProps, DateTimeValue
  • packages/quill/packages/components/src/date-picker.tsxDatePickerProps (single date, added during this migration)
  • packages/quill/packages/components/src/date-time-ranges.tsquickRanges, CUSTOM_RANGE
  • packages/quill/packages/components/src/use-calendar.ts — headless useCalendar hook

Feature comparison (as of the original audit)

Feature LemonUI Quill Notes
Single-date selection LemonCalendarSelect ✅ (added) Quill DatePicker sibling added in #65098
Date-range selection LemonCalendarRange
Inline (no popover) variant ...Inline ⚠️ Quill is a panel; caller embeds it
Quick-range presets ✅ (15) Quill-only
Time granularity (day / hour / minute) granularity ⚠️ day/minute mapped via seam; no hour-only
"Include time" toggle showTimeToggle / onToggleTime ✅ (added) showTimeToggle/onIncludeTimeChange added
12h vs 24h format use24HourFormat Quill hardcodes 24h numeric inputs
Restrict to past / upcoming dates selectionPeriod ⚠️ decided: express via min/maxDate
Explicit min/max bounds ⚠️ minDate/maxDate
Date segment order (MDY/DMY/YMD) dateFormat Quill-only
Clearable clearable ✅ (seam)
Placeholder placeholder ✅ (seam)
Custom per-date button props getLemonButtonProps ✅ typed getDateState redesigned in #65056
Controlled popover visibility + built-in trigger visible, buttonProps ✅ (seam) seam owns trigger/popover
Week start day weekStartDay weekStartsOn parity
Multiple months months ⚠️ seam falls back for months > 1
Underlying value type dayjs native Date seam converts at the boundary
Headless hook useCalendar Quill-only

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with frontend/src/lib/components/DatePicker/DatePicker.tsx and compare the LemonUI sources with the Quill picker files listed in the inventory. Then inspect the remaining LemonCalendarSelectInput callers and the range call sites in frontend/src/lib/components/DateFilter/DateFilter.tsx; done means the listed parity gaps, seam bugs, migrations, and rollout conditions are resolved.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, testing-qa
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
30/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.