Base components
Date and time input
A typed date, time or date and time field split into parts (month, day, year, hour, minute) in the order of the user's locale, where the focused part gets a soft highlight and the field moves like Input.
Live preview
Loading preview
Three fields at size md: "Start date" (date), "Reminder" (time, 12 hour clock) and "Meeting" (date and time, hint "Use your local time zone.", min today, error "Enter a date and time in the future."). Replay types a full date and time into Meeting.
Installation
Run this in your app after the one-time setup on the Getting started page.
$ npx @ganisigit/lunia-ds add date-time-inputUsage
The demo above, as code.
'use client';
import * as React from 'react';
import { TooltipProvider } from '../tooltip/tooltip';
import { DateTimeInput } from './date-time-input';
const PAST_MEETING = '2025-01-10T09:00';
const NEXT_MEETING = '2028-04-16T10:30';
const pad = (n: number) => String(n).padStart(2, '0');
const nowIso = () => {
const d = new Date();
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
export default function DateTimeInputDemo({ replay = 0 }: { replay?: number }) {
const [meeting, setMeeting] = React.useState<string | null>(PAST_MEETING);
const min = React.useMemo(nowIso, []);
const past = meeting !== null && meeting < min;
React.useEffect(() => {
if (replay === 0) return;
setMeeting(null);
const timer = window.setTimeout(() => setMeeting(NEXT_MEETING), 500);
return () => window.clearTimeout(timer);
}, [replay]);
return (
<TooltipProvider>
<div className="flex w-full max-w-xs flex-col gap-6">
<DateTimeInput label="Start date" locales="en-US" defaultValue="2028-04-16" />
<DateTimeInput
label="Reminder"
locales="en-US"
granularity="time"
hourCycle={12}
defaultValue="09:15"
/>
<DateTimeInput
label="Meeting"
locales="en-US"
granularity="datetime"
min={min}
value={meeting}
onValueChange={setMeeting}
helpText="Guests see this time in their own time zone."
hint={past ? 'Enter a date and time in the future.' : 'Use your local time zone.'}
/>
</div>
</TooltipProvider>
);
}
API
Parts: field (label row, help icon, hint from input), group (border box), calendar or clock icon, segments (one per date or time part, with the locale separators between them), help icon (optional, at the end of the field).
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | null | undefined | Value (controlled) as a local ISO string: "2028-04-16" for dates, "10:30" for times, "2028-04-16T10:30" for both. null is an empty field. |
| defaultValue | string | null | null | Value when uncontrolled. |
| granularity | "date" | "time" | "datetime" | "date" | Which parts to show. |
| hourCycle | 12 | 24 | undefined | 12 or 24 hour clock. Defaults to the locale's clock. |
| min | string | undefined | Earliest value, same ISO form as value. Earlier values make the field invalid. |
| max | string | undefined | Latest value, same ISO form as value. |
| locales | string | "en-US" | Locale for the part order, separators and placeholders. |
| size | "sm" | "md" | "lg" | "md" | Field height 40, 44 or 48px, matching Input. |
| label | string | undefined | Visible label above the field. |
| hint | string | undefined | Hint line under the field; shows the error text when invalid. |
| helpText | string | undefined | Text for the help icon (from input). |
| required | boolean | false | Shows the required mark and sets aria-required. |
| invalid | boolean | false | Error border, error icon and red hint, as in Input. Also set by the field itself when the value is outside min and max. |
| disabled | boolean | false | Turns the field off (50% opacity). |
| readOnly | boolean | false | The value can be read but not changed. |
| name | string | undefined | Form field name; the ISO string is sent. |
| Event | Description |
|---|---|
| valueChange | The value changed: an ISO string when every part is filled, null when a part is cleared. |
| Slot | Description |
|---|---|
| icon | Replaces the calendar icon (a clock icon is the default for granularity time). |
Motion
Every row names a token from the Motion page. The last column says what happens when the user asks for reduced motion.
| ID | Trigger | Element | Property | From → to | Timing | Reduced motion |
|---|---|---|---|---|---|---|
| DTI1 | hover the field | group border | border-color | border-control → border-control_hover | duration-sm (200)ease-out | Swaps at once |
| DTI2 | keyboard focus in any segment | group | border-color, box-shadow | border-control, shadow-xs → border-brand, focus-ring-shadow-xs | duration-2xs (120)ease-out | Shows at once |
| DTI3 | a segment gets focus | segment | background-color, color | transparent, text-primary → bg-brand-primary, text-brand-secondary | duration-2xs (120)ease-out | Swaps at once |
| DTI4 | a segment is filled (placeholder -> number) | segment text | color | text-placeholder → text-primary | duration-sm (200)ease-out | Swaps at once |
| DTI5 | invalid -> true | group border, error icon, hint | border-color, opacity, color | border-control, 0, text-tertiary → border-error, 1, text-error-primary | duration-sm (200)ease-out | Swaps at once |
Accessibility
- The group has role group and is named by the label; each segment has role spinbutton with aria-label ("Month", "Day", "Year", "Hour", "Minute", "AM or PM"), aria-valuemin, aria-valuemax, aria-valuenow and aria-valuetext.
- Keyboard: typing digits fills the segment and moves to the next one when it is full; Left and Right move between segments; Up and Down change the focused part; Backspace clears it.
- Each segment is a tab stop, so Tab and Shift Tab also move between segments and then leave the field, as in Reka DateField.
- Separators and the icon are aria-hidden; the full value is read through aria-valuetext of each part and the hint.
- The label, hint and invalid state are linked as in Input; a hidden native input with the ISO string takes part in forms.
- Reduced motion: colors swap at once; nothing moves in this component.