Oihana Next UI - 0.48.0

Application logo

Date Components

Our own dayjs-based calendar — single date and range, one or two months, with shortcuts — the three grid pickers that select a period without ever showing a day, and the field pickers built on both.

Calendar — single date

A self-contained, dayjs-based month calendar — no react-day-picker / date-fns. Locale-aware : month / weekday names and the first day of week follow the language (switch 🇫🇷 / 🇬🇧). Click the month or year in the header to jump quickly (month grid → year grid → back to days).

lumamejevesadi

Selected : Thu Oct 22 2026

clearable — click the selected day again, or press Esc, to clear.

With min / max bounds (today → +1 month)

The bounds also stop navigation : the ‹ › arrows go dead rather than walking into a month that is entirely out. Same in the quick pickers — open the month grid and its year chevrons are bounded, open the year grid and so is its 12-year pagination. Only min / max bound navigation ; a month blocked by disabledMonths stays reachable, otherwise the month behind it would not be.

lumamejevesadi
Bounded navigation over a wider span

Bounds set to 2026 → 2028, so the year and page arrows are reachable enough to try. Click the year in the header : the 12-year page stops paging once even its last year falls short of the bound.

lumamejevesadi
Opens on a given month (defaultMonth)

No value selected — the view starts on January 2030 via defaultMonth.

lumamejevesadi
Animate the month change (animate)

The month slides in the direction the arrows point, through the browser's View Transition API. Off by default. The two calendars below differ by that single prop — use the arrows on both and compare. A reader who asks for reduced motion gets a fade instead of the travel, and a browser without the API simply changes the month as it always did.

Default
lumamejevesadi
animate
lumamejevesadi
Force the first day of week (weekStartsOn)

Independent of the locale : weekStartsOn="sun" starts weeks on Sunday even in 🇫🇷 (which is Monday by default). Accepts 0–6 or 'sun'…'sat'.

dilumamejevesa
Block whole weekdays (disabledWeekdays)

Accepts 0–6 (0 = Sunday) or 'sun'…'sat', alone or in an array. The numbers are absolute — unlike the grid order, they never follow weekStartsOn. The two calendars also show the two renderings : a blocked weekday is muted only (it is the shape of the calendar), a blackout date stays struck through (it is an exception).

week-ends off
lumamejevesadi
week-ends off + blackout dates
lumamejevesadi
Block months and years (disabledMonths, disabledYears)

Click the month or the year in the header to open the quick pickers — that is where the rules show. disabledMonths takes 0–11 (that month every year), a { year , month } pair, an array of those or a ( year , month ) predicate ; disabledYears takes a year, a { from , to } range (either bound optional), an array or a ( year ) predicate. Rules cascade downwards : a blocked year greys its twelve months, a blocked month greys all its days. Navigation stays free — you can still walk through a blocked month to reach the next one.

July and August off, every year
lumamejevesadi
nothing from 2028 on
lumamejevesadi
Months emptied by their days (deriveEmptyMonths)

Every day of March 2027 is blacked out, but nothing declares the month itself blocked — so by default its button in the quick picker stays live and lands you on a fully greyed grid. deriveEmptyMonths makes the picker scan the month and strike it through instead. It is opt-in because it costs that scan, and it is never applied to years. Open the month picker on 2027 to compare.

default — March stays live
lumamejevesadi
deriveEmptyMonths — March struck through
lumamejevesadi

Calendar — range

The same calendar in range mode, over one or two months, with a live hover preview and optional shortcuts (presets).

Two months (auto: 2 on desktop, 1 on mobile)

Shortcut labels follow the language. On mobile they become a swipeable strip.

lumamejevesadi

From — to —

Single month range
lumamejevesadi
Blackout dates (disabledDates)

The 10th and the 18th–22nd are blocked (struck through, inert). By default a range stops before a blocked day; the second calendar sets allowDisabledInRange so a range may span them. A spanned day keeps the band's square corners but never its primary fill — it is crossed, not selected.

default — cannot span
lumamejevesadi
allowDisabledInRange — may span
lumamejevesadi
Ranges over blocked weekdays

Week-ends are blocked. The rule applies to the range exactly as a blackout date does : by default the selection stops on the Friday, so no range can be longer than a working week. Add allowDisabledInRange to let it span the week-ends — select a fortnight on the right and the two week-ends show as neutral gaps inside the band : crossed, not selected.

default — one working week at most
lumamejevesadi
allowDisabledInRange — may span
lumamejevesadi
Shortcuts with icons & a divider

Each shortcut carries an Icon; a { divider: true } item splits the list (a vertical rule in the mobile strip, a horizontal one in the column).

lumamejevesadi

Input date picker

The masked field and the calendar share one value : typing updates the calendar, picking a day fills the field and closes. The popover opens as a dropdown anchored to the field on desktop and as a centered modal on mobile — force either with display="dropdown" | "modal".

Type or pick; × to clear.

Out-of-range days are disabled

Selected (responsive) : —

Sizes
Alternative format & disabled

Custom mode + separator

Rules on the field, not only on the grid (strict)

disabledDates, disabledWeekdays, disabledMonths and disabledYears are now first-class props of the picker : they reach the calendar, and with strict they also police what you type. Without it the keyboard accepts what the click refuses. Try typing a Saturday in both fields — the left one emits it, the right one keeps the text, goes into error and stays silent on onDate.

The grid refuses them, the keyboard does not

Typing a Saturday puts the field in error

Lenient emitted : — — strict emitted : — — last refused : —

Input date range picker

The range mirror : it maps the field's { start, end } to the calendar's { from, to } and only closes once both endpoints are picked. Same responsive popover, with a wider dual-month dropdown on desktop.

Type or pick two days; × to clear.

Out-of-range days are disabled

Selected (responsive) : —

Custom range separator & disabled

rangeSeparator=' → '

Strict range

Same four props, same strict. It validates the endpoints, not the span : whether a range may cross a blocked day is allowDisabledInRange's business, and that belongs to the grid. Type a period starting on a Saturday.

Either endpoint on a week-end is refused

The 10th and the 18th–22nd of this month are refused

Apply / Cancel footer (footer)

With a footer, picking is deferred : the field commits on Apply and reverts on Cancel / Esc. Choose where it shows : "always", "mobile" (below md only), or "desktop" (md+ only). Resize the window to see the mobile / desktop ones toggle.

Footer only below md

Footer only at md+

Regression — inline handlers, fresh-object setState

Both fields get an onDate / onDateRange arrow recreated on every render, whose setState is never idempotent (a counter plus a fresh object). The emit counters must settle — one emit on mount, one per real change. If they run away (« Maximum update depth exceeded »), the parse effect is looping again.

onDate emits : 0 · last : —

onDateRange emits : 0 · last : —

Input date-time picker

A date field + a time field sharing one popover that hosts the calendar and the time columns. The value is the combined string (e.g. 25/12/2026 14:30); the parsed Date comes via onDateTime. The popover stays open and closes on outside-click / Esc — or via the optional footer.

Pick a day then a time; × to clear.

Picks are deferred until Apply

Selected : —

Width — a month that fills what it is given

A day cell is no longer a fixed square : the seven columns are minmax( cellMin , cellMax ) , so a month grows into the width it is given, stops at its ceiling, and centres in whatever is left. Where nothing imposes a width — inside a picker's popover, which sizes itself to its content — the columns fall back to their floor and the calendar is exactly what it has always been.

Drag the handle at the bottom-right corner of the box below.

lumamejevesadi

Three boxes of the same width, and the only difference is the ceiling. The last one has it set to the floor, which is how the whole behaviour is turned off — one prop, no second switch to remember.

default ceiling

lumamejevesadi

cellMax={ 64 } — roomier

lumamejevesadi

cellMax={ CALENDAR_CELL_MIN } — off

lumamejevesadi

The quick month and year grids follow the same width, and two months share it — open the header of a wide calendar, or set months=2, and the panel must not jump.

lumamejevesadi
lumamejevesadi

Date label

Written in full
16 septembre 2026
16/09/2026
16 septembre 2026 12:32
16/09/2026
In a sentence the screen owns
Opened : 23 septembre 2026
Last counted : 22/09/2026
A day served alone

« 2017-05-31 » names a day, with no hour and no zone. It is read in the application's time zone, so a server running UTC and a browser elsewhere write the same text. With a pattern carrying the hour it still reads midnight — an hour nobody stated ; datePattern writes such a value without it.

Created : 31/05/2017 00:00:00
Created : 31/05/2017
Créé : 31/05/2017
Modifié : 22/09/2026 à 10:15:00
Against the clock

Reload and watch these two : they are written in full for an instant — what the server sent — then turn relative, once the page is past hydration.

22 septembre 2026
16 septembre 2026
And it moves

This one takes its instant after mount, so it starts at « a few seconds ago » and moves on its own — every 5 seconds here, once a minute by default. It carries nothing but its icon until the effect has run.

Nothing to write
No date, no stand-in : a lone icon.
No date, with a stand-in :
Never
An unreadable date, with a stand-in :
Unknown

Meta dates

The pair
Créé : 16/09/2026 à 12:32:05
Modifié : 22/09/2026 à 10:15:00
Créé : 16/09/2026 à 12:32:05
Modifié : 22/09/2026 à 10:15:00
One of the two, under another name
Créé : 16/09/2026 à 12:32:05
Créé : 23/09/2026 à 19:45:00
Modifié : 22/09/2026 à 10:15:00
Narrow

The pair wraps rather than overflowing — the box below is 260 px wide.

Créé : 16/09/2026 à 12:32:05
Modifié : 22/09/2026 à 10:15:00
Nothing at all

A record carrying neither date renders nothing — there is no row below this line.

Pickers inside a Modal

The Popover panel portals into the host dialog[open](body-level content under a modal dialog is inert and paints below the top layer). Escape closes the topmost surface only : the picker when open, then « Éditer ».

Date : — · Heure : — · Date-heure : —

Pickers inside a Popover

Le calendrier d'un sélecteur est rendu à part, dans la page, et pourtant un clic sur un jour ne ferme pas le panneau qui le contient : pour le Popover, ce clic vient de l'intérieur.

Du — au —

Year picker

The 12-year grid the calendar navigates with, turned into a control that owns a value : clicking a year selects it instead of jumping somewhere. The value is a plain number. Paged ±12 by the « » chevrons.

2022 – 2033

Selected : 2026

clearable — click the selected year again, or press Esc, to clear.

Bounds and blocked years

The min and max bounds take a Date or a year, and a year bound covers the whole year. They also stop the paging : a page whose every year is out is unreachable rather than empty. disabledYears greys years out without blocking navigation — here, everything that is not a leap year.

2022 – 2033

Selected : —

Month picker

A month of the year with no year attached — a recurring month, an anniversary, a season. The value is the month index, from 0 to 11. Names and their order follow the language (switch 🇫🇷 / 🇬🇧).

Selected : 9 — octobre

clearable — click the selected month again, or press Esc, to clear.

Full names, three per row — and no summer

Abbreviations fit four to a row ; whole names ask for three, through the columns and the labelFormat props. Blocked months come from the disabledMonths rule, which takes an index, a { year , month } pair, an array of those, or a predicate.

Selected : —

Month and year picker

The calendar's quick navigation promoted to a control of its own : the months grid with its ‹ year › header, and the year itself opening the 12-year grid behind it. The same chain as the calendar, one step shorter — years → month, where the calendar goes years → month → day. Only the month commits ; a year merely carries you to its months. The value is a Date on the first day of the month.

Selected : —

Click the year in the header to open the year grid.

Bounded to the current year

With min on the 1st of January and max on today, the ‹ › chevrons of the header go dead at both ends and the months still to come are out of reach — a billing period cannot be in the future.

Billing period : janvier 2026

In a popover, or a modal

The three are plain blocks with no opinion about where they live : a page, a card, or the Popover, which is a dropdown anchored to its trigger on md+ screens and a bottom-sheet below (display="responsive"). Closing on pick is the caller's decision — a picker that closes itself could not be used inline — which is exactly what the close handed to the panel function is for.

Both buttons below are a PopoverButton, which owns the position hook, the open state and the anchoring. Nothing is left at the call site but the button and the panel — the trigger is its own anchor, and it carries the type="button", the aria-haspopup and the aria-expanded a hand-written one forgets.

responsive — dropdown on md+, sheet below
display="modal" — centered card, backdrop

Input month and year picker

The packaged version of what the section above assembles by hand : the masked field InputDate in its mode mm/yyyy, with the grid in the same responsive popover. The twin of InputDatePicker one granularity up — same props, same handlers, no days. Typing moves the grid, picking a month fills the field and closes the popover, and onDate always hands back the first day of the month, whichever side it came from.

Type 09/2026, or open the grid

Text : —

Date : —

Bounded to today — a billing period cannot be in the future.

strict — refusing what the grid would refuse

A month typed at the keyboard never goes through the grid, so the rules are asked here too. The second half of the year is blocked : typing a 09/2026 puts the field into error rather than emitting. The question asked is the getMonthReason one, never the day rules — a month partly inside the bounds is not blocked, its days are.

Accepted : —

Last refused : —

Bounds given as years cover the whole year.

Input year picker

One granularity further up : four digits and nothing to separate. It speaks the language of the grid it wraps — onYear hands back a plain number, as YearPicker does, rather than a Date standing in for an integer. An onDate on the 1st of January is offered beside it for symmetry with the rest of the family. Only the rule disabledYears is accepted : neither a day nor a month exists here.

Type four digits, or open the grid

Text : —

Year : —

Bounded 1950 → this year, and every decade's first year greyed out. The mask rewrites a year typed outside the bounds onto the nearest one, so the field and the value never disagree.

useDateFormat — fr · Europe/Paris

Instant (UTC)useDateFormatUTCMachine (sans fuseau)
2026-09-17T08:32:00Z17 septembre 2026 10:3217 septembre 2026 08:32
2026-12-17T08:32:00Z17 décembre 2026 09:3217 décembre 2026 08:32
2026-09-17T21:45:00Z17 septembre 2026 23:4517 septembre 2026 21:45
2026-09-17T22:15:00Z18 septembre 2026 00:1517 septembre 2026 22:15
Patternfren
LL17 septembre 2026September 17, 2026
LLL17 septembre 2026 10:32September 17, 2026 10:32 AM
L17/09/202609/17/2026
DD/MM/YYYY17/09/202617/09/2026
dddd LL HH:mmjeudi 17 septembre 2026 10:32Thursday September 17, 2026 10:32

Valeurs vides : null → nullundefined → null"" → null"not a date" → null