Date picker: LemonUI vs Quill feature parity audit
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
LemonCalendarper-cell hooks (getLemonButtonProps/getLemonButtonTimeProps) into typedgetDateState/getTimeState, removing the leakedLemonButtonAPI. - #65059 — the
DatePickerseam (LemonUI inside), with 2 pilot callers migrated (batch-export end date, visual-review quarantine expiry). - #65098 — a Quill single-date
DatePicker(sibling ofDateTimePicker, nativeDate), sharing acalendar-grid+segmented-date-inputwith the range picker. - #65102 / #65553 — jest+RTL for
quill-componentsand wiring the quill packages into the CI jest run. - #65519 — flip the seam to Quill behind
quill-date-picker, with amaxDatebound 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
selectionPeriodLimitprop.
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 selection — PORTED (#65098). Quill
DatePickeris a single-date sibling ofDateTimePicker(Datein/out), not amodeprop. - 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 addedshowTimeToggle/onIncludeTimeChangeto 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: usemin/maxDate. TheselectionPeriod/selectionPeriodLimitpair is a confusing way to express bounds;min/maxDate(Quill-native, clear) is the API going forward.selectionPeriodLimithad zero callers and was retired (#65723);maxDatewas added to the seam (#65519). Still to wire: mappingselectionPeriod='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 typedgetDateState/getTimeStatethat mirror Quill'sisStart/isEnd/isBetweenvocabulary and own the styling internally — no leakedLemonButtonAPI. - Display format string (
format) — PORTED at the seam. The seam honours a dayjs format string for the trigger label; the Quill panel usesdateFormatsegment 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 avisible/onOpen/onClickOutside/onClosetrio 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()thenonChange(dayjs(next))inDatePicker.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, YYYYformat 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
LemonSelectin 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. -
maxDatebounds only the Quill arm. Per the seam doc, the LemonUI fallback ignoresmaxDateand 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
onChangeinto non-nullable caller fields.DatePicker'sonChange: (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 passesclearable). A latent trap whenclearableis 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
LemonCalendarSelectInputonto 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/maxDatemapping. - Fix the deferred Quill-path bugs above (timezone, localized format, stuck tooltip, trigger styling) — these gate the rollout.
- Build a
DateRangePickerseam for the 3 range call sites (all infrontend/src/lib/components/DateFilter/DateFilter.tsx). - Roll the
quill-date-pickerflag 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.tsx—LemonCalendarProps(grid primitive)frontend/src/lib/lemon-ui/LemonCalendar/LemonCalendarSelect.tsx—LemonCalendarSelectProps+LemonCalendarSelectInputProps(single date)frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRange.tsx—LemonCalendarRangeProps(range)frontend/src/lib/lemon-ui/LemonCalendarRange/LemonCalendarRangeInline.tsx— inline range
Quill (@posthog/quill-components):
packages/quill/packages/components/src/date-time-picker.tsx—DateTimePickerProps,DateTimeValuepackages/quill/packages/components/src/date-picker.tsx—DatePickerProps(single date, added during this migration)packages/quill/packages/components/src/date-time-ranges.ts—quickRanges,CUSTOM_RANGEpackages/quill/packages/components/src/use-calendar.ts— headlessuseCalendarhook
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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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