Skip to content

feat(Calendar): add month/year picker modes and view switching#5981

Open
onmax wants to merge 7 commits intonuxt:v4from
onmax:feat/calendar-month-year-picker
Open

feat(Calendar): add month/year picker modes and view switching#5981
onmax wants to merge 7 commits intonuxt:v4from
onmax:feat/calendar-month-year-picker

Conversation

@onmax
Copy link
Copy Markdown
Contributor

@onmax onmax commented Feb 2, 2026

🔗 Linked issue

Resolves #5842, resolves #3652
Related #3094

❓ Type of change

  • 📖 Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • 👌 Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

📚 Description

  1. Add supports for calendar month picker modes via type prop
  2. Add support for year picker modes via type prop
  3. Interactive view switching (day -> month -> year). Maybe too opinionated?

Changes:

  • Add type prop (date | month | year) for standalone pickers
  • Add view / defaultView props for view state control
  • Clickable heading to switch views (day -> month -> year) when type="date"
  • New theme slots: monthGrid, monthCell, yearGrid, yearCell
  • New slots: month-cell, year-cell for custom rendering

📸 Screenshots

Feature Screenshot
Month Picker (type="month") image
Year Picker (type="year") image
View Switching (heading click) video below
Cap.2026-02-04.at.14.31.40.mp4

📝 Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@github-actions github-actions Bot added the v4 #4488 label Feb 2, 2026
@onmax onmax force-pushed the feat/calendar-month-year-picker branch from d181f50 to 28da0df Compare February 2, 2026 15:41
@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Feb 2, 2026

@benjamincanac what do you think about this PR? Do you like the API?

There are some styles I would like to improve and nail down. But I think is mostly ready :)

Let me know your thoughts! Thank you!

Once we decide to move forward I will ammend PR fixing CI and fixing UI issues

Copy link
Copy Markdown
Member

Thanks for the PR! I do like the API with the type prop approach, it's consistent with other components.

However, it adds lots of code since there are no primitives for this in Reka UI. @J-Michalek what do you think about this? Are you aware of such thing being added in Reka UI any time soon?

@J-Michalek
Copy link
Copy Markdown
Contributor

Well there is an issue open for 8 months unovue/reka-ui#1933 and the activity in the repo is mild at best, but I think we should rely on RekaUI to provide these inputs...

Perhaps the author of this PR would be interested in implementing in RekaUI?

@caiotarifa
Copy link
Copy Markdown
Contributor

caiotarifa commented Feb 3, 2026

I’m the author of the feature request in Reka UI (unovue/reka-ui#1933), and it has already gathered meaningful community interest (19+ reactions).

As mentioned above, this PR adds a fair amount of code mainly because Reka UI doesn’t provide primitives for month/year picking yet.

@onmax, if you’re open to it, migrating this PR to add MonthPicker and YearPicker primitives in Reka UI would be an excellent win for the ecosystem (as @J-Michalek suggested).

@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Feb 3, 2026

Thanks for the feedback. I will prepare a PR for Reka UI.

@benjamincanac, should I keep the behaviour shown in the video? I am not 100% this is the best ux 🤔

@benjamincanac
Copy link
Copy Markdown
Member

I think I like it yes, it avoids having to implement popover and lets us render a Calendar only for months at the same time as selecting a month for a normal calendar.

@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Feb 3, 2026

Ok, i won't bother you again 😬,

once reka ui is released and this pr is ready I will mark this pr ready and i will ping you.

Feel free to post any feedback though. Most of the code is ready. I am just testing it throughly :)

@onmax onmax force-pushed the feat/calendar-month-year-picker branch 2 times, most recently from 4d64993 to ae0edf7 Compare February 3, 2026 17:54
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 3, 2026

npm i https://pkg.pr.new/@nuxt/ui@5981

commit: fe8227a

@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Feb 4, 2026

For reference: much better approach imho

image

@sewalsh
Copy link
Copy Markdown

sewalsh commented Feb 4, 2026

Great to see work on this! I've long missed it since version 2.

Just my 2c. I think a 3x4 grid looks neater as was used in v2: https://ui2.nuxt.com/components/date-picker#datepicker

@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Mar 4, 2026

fixed

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/runtime/components/Calendar.vue (1)

208-212: Consider using setLocale() instead of recreating the formatter.

Reka UI's useDateFormatter provides a setLocale() method specifically for updating the locale. This is more efficient than recreating the formatter instance on each change:

💡 Suggested refactor
-const formatter = shallowRef(useDateFormatter(code.value))
-
-watch(() => code.value, (value) => {
-  formatter.value = useDateFormatter(value)
-})
+const formatter = useDateFormatter(code.value)
+
+watch(() => code.value, (value) => {
+  formatter.setLocale(value)
+})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/runtime/components/Calendar.vue` around lines 208 - 212, The current code
recreates the date formatter on each locale change by assigning formatter.value
= useDateFormatter(value); instead use the formatter's built-in updater: keep
formatter as shallowRef(useDateFormatter(code.value)) and in the watch callback
call formatter.value.setLocale(value) (ensure setLocale exists on the returned
object) instead of replacing the whole formatter instance to improve efficiency
and preserve any internal state or subscriptions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/runtime/components/Calendar.vue`:
- Around line 208-212: The current code recreates the date formatter on each
locale change by assigning formatter.value = useDateFormatter(value); instead
use the formatter's built-in updater: keep formatter as
shallowRef(useDateFormatter(code.value)) and in the watch callback call
formatter.value.setLocale(value) (ensure setLocale exists on the returned
object) instead of replacing the whole formatter instance to improve efficiency
and preserve any internal state or subscriptions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6c23c1df-28da-43ee-8187-b64d3c7ae764

📥 Commits

Reviewing files that changed from the base of the PR and between 5581f82 and 6959644.

⛔ Files ignored due to path filters (2)
  • test/components/__snapshots__/Calendar-vue.spec.ts.snap is excluded by !**/*.snap
  • test/components/__snapshots__/Calendar.spec.ts.snap is excluded by !**/*.snap
📒 Files selected for processing (6)
  • docs/app/components/content/examples/calendar/CalendarMonthPickerExample.vue
  • docs/app/components/content/examples/calendar/CalendarYearPickerExample.vue
  • docs/content/docs/2.components/calendar.md
  • src/runtime/components/Calendar.vue
  • src/theme/calendar.ts
  • test/components/Calendar.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/app/components/content/examples/calendar/CalendarYearPickerExample.vue

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/runtime/components/Calendar.vue`:
- Around line 619-635: The inline branch that renders <component
:is="picker.root"> is dropping root-level props by passing a hand-picked set
(pickerValueProps, placeholder, locale, dir, minValue, maxValue, disabled,
readonly, class) instead of reusing the same root prop assembly used by the
day-calendar path; fix this by using the same calendarRootProps construction
(the same prop spread used for day mode) when rendering picker.root and then
explicitly override only the value/placeholder wiring and event handlers (keep
picker.onUpdate and onPickerPlaceholderUpdate) so documented root props like as
and other root-level options are preserved across type !== 'date' modes.
- Around line 241-245: The watcher currently re-applies props.defaultView after
mount, turning it into live state; change the logic so defaultView only seeds
internalView once and is not watched thereafter. Concretely: stop watching
props.defaultView (remove it from the watcher), initialize internalView from
defaultView on mount (or only when internalView is undefined), and update the
watcher to only react to props.type and props.view so that internalView is
updated when the component is controlled (props.view changes) or when props.type
changes (in which case call getDefaultView(type, defaultView) to compute a new
seed if internalView is undefined). Ensure you reference and update the existing
watcher and the initialization for internalView and use getDefaultView(type as
CalendarType, defaultView as CalendarView | undefined) where needed.
- Around line 364-379: Remove the no-op expressions causing the linter error by
deleting the stray `code.value` lines in the functions `formatMonthLabel` and
`formatYearLabel`; leave the rest of each function intact so they continue to
call `formatter.value.custom(date.toDate(getLocalTimeZone()), {...})` and fall
back to `String(date.month)` / appropriate fallback — this removes the
unused-expression warnings from `@typescript-eslint/no-unused-expressions`
without changing reactivity (the locale sync is already handled by the watcher).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ba04acfe-5bd5-4986-bc24-479bb70b5631

📥 Commits

Reviewing files that changed from the base of the PR and between 6959644 and abb210e.

📒 Files selected for processing (1)
  • src/runtime/components/Calendar.vue

Comment thread src/runtime/components/Calendar.vue Outdated
Comment thread src/runtime/components/Calendar.vue Outdated
Comment thread src/runtime/components/Calendar.vue
Copy link
Copy Markdown
Contributor

@howwohmm howwohmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

impressive feature — the architecture of splitting day/month/year into distinct root components with a dynamic picker computed is clean.

two things I noticed:

  1. stale label reactivity in monthPicker/yearPicker computed. the computed properties extract .value from reactive label refs (prevYearLabel.value, nextYearLabel.value) inside the computed body. the string is captured at evaluation time, but the computed's primary dependency is props.range, not the label refs themselves. if the locale changes at runtime, the labels won't update because the computed won't re-evaluate. either pass the refs directly and resolve .value in the template, or restructure so the computed reads from the refs on each access.

  2. watch on [props.type, props.view] — unsafe destructure on first run. the callback destructures [previousType] from the old value, which is undefined on the initial invocation. the type !== previousType check works by coincidence (props.type defaults to 'date' which isn't undefined), but an explicit if (!previousType) return guard would be more robust.

also worth noting: the heading slot's value field now changes semantics depending on the view (e.g. "2025" in year view vs "January 2025" in day view). existing consumers using #heading="{ value }" won't break, but the string they get will differ — might be worth a migration note in the docs.

Copy link
Copy Markdown
Contributor

@howwohmm howwohmm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correction on my earlier comment about stale label reactivity — I was wrong on that point.

Vue's reactivity system tracks all .value accesses inside a computed() getter, so prevYearLabel.value and nextYearLabel.value ARE registered as dependencies of the monthPicker/yearPicker computed. the labels will correctly update on locale change. apologies for the false alarm there.

the watch destructure point (undefined old value on first run) and the heading slot semantics observation still stand, but those are minor.

@mateusznarowski
Copy link
Copy Markdown
Contributor

@onmax In the latest version of reka-ui, I've added the missing inputs, so you can switch your imports of MonthPicker and YearPicker to the namespaced versions. unovue/reka-ui#2571

@onmax onmax force-pushed the feat/calendar-month-year-picker branch from 5940ea9 to f3c9965 Compare April 7, 2026 14:02
@onmax
Copy link
Copy Markdown
Contributor Author

onmax commented Apr 7, 2026

I have updated htis pr:

  1. I rebased it onto current v4
  2. resolved the calendar conflicts
  3. switched the month/year picker imports to the new namespaced Reka API
  4. refreshed the affected snapshots

Comment thread src/theme/calendar.ts Outdated
yearGrid: 'w-full select-none space-y-1 focus:outline-none',
yearGridRow: 'grid grid-cols-4 gap-1',
yearCell: 'relative text-center',
yearCellTrigger: ['relative flex w-full items-center justify-center rounded-md whitespace-nowrap tabular-nums focus-visible:ring-2 focus:outline-none data-disabled:text-muted', options.theme.transitions && 'transition']
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onmax Why duplicate all these slots? They should be identical for day, month and year no? Otherwise we could use a type variant that targets cell, cellTrigger and cellWeek instead of duplicating everything, what do you think?

Copy link
Copy Markdown
Contributor Author

@onmax onmax Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, I would group month and year

day is not fully identical to month/year. Day view has week headers/numbers and different trigger states, so I would not force all three into literally the same classes. But month and year are close enough. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@benjamincanac I have pushed the changes I sent in my previous message, and also cleaned up a bit, as well as rebased again.

Let me know if there is something else to fix and get this pr merged!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

v4 #4488

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add <UMonthPicker> and <UYearPicker> components (requires RekaUI Calendar view modes) Calendar: improve month and year select

7 participants