Lunia DS

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.

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-input

Usage

The demo above, as code.

demo.tsx
'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).

PropTypeDefaultDescription
valuestring | nullundefinedValue (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.
defaultValuestring | nullnullValue when uncontrolled.
granularity"date" | "time" | "datetime""date"Which parts to show.
hourCycle12 | 24undefined12 or 24 hour clock. Defaults to the locale's clock.
minstringundefinedEarliest value, same ISO form as value. Earlier values make the field invalid.
maxstringundefinedLatest value, same ISO form as value.
localesstring"en-US"Locale for the part order, separators and placeholders.
size"sm" | "md" | "lg""md"Field height 40, 44 or 48px, matching Input.
labelstringundefinedVisible label above the field.
hintstringundefinedHint line under the field; shows the error text when invalid.
helpTextstringundefinedText for the help icon (from input).
requiredbooleanfalseShows the required mark and sets aria-required.
invalidbooleanfalseError border, error icon and red hint, as in Input. Also set by the field itself when the value is outside min and max.
disabledbooleanfalseTurns the field off (50% opacity).
readOnlybooleanfalseThe value can be read but not changed.
namestringundefinedForm field name; the ISO string is sent.
EventDescription
valueChangeThe value changed: an ISO string when every part is filled, null when a part is cleared.
SlotDescription
iconReplaces 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.

IDTriggerElementPropertyFrom → toTimingReduced motion
DTI1hover the fieldgroup borderborder-colorborder-control → border-control_hoverduration-sm (200)ease-outSwaps at once
DTI2keyboard focus in any segmentgroupborder-color, box-shadowborder-control, shadow-xs → border-brand, focus-ring-shadow-xsduration-2xs (120)ease-outShows at once
DTI3a segment gets focussegmentbackground-color, colortransparent, text-primary → bg-brand-primary, text-brand-secondaryduration-2xs (120)ease-outSwaps at once
DTI4a segment is filled (placeholder -> number)segment textcolortext-placeholder → text-primaryduration-sm (200)ease-outSwaps at once
DTI5invalid -> truegroup border, error icon, hintborder-color, opacity, colorborder-control, 0, text-tertiary → border-error, 1, text-error-primaryduration-sm (200)ease-outSwaps 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.