Date Time Input
Native date, time and date-time inputs, plus start/end range variants of each.
Installation
npx kosmesis add date-time-inputpnpm dlx kosmesis add date-time-inputyarn dlx kosmesis add date-time-inputbunx kosmesis add date-time-inputCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import { cn } from "@/lib/utils";export type TemporalInputType = "date" | "time" | "datetime-local";export interface TemporalInputProps { type: TemporalInputType; /** `"YYYY-MM-DD"`, `"HH:mm[:ss]"` or `"YYYY-MM-DDTHH:mm"`, matching the native input's value format. */ value?: string; defaultValue?: string; min?: string; max?: string; step?: number; onChange?: (value: string) => void; disabled?: boolean; readonly?: boolean; required?: boolean; invalid?: boolean; name?: string; class?: string; id?: string; "aria-label"?: string;}@Component()export class TemporalInput extends StatefulComponent { @Prop() type: TemporalInputType = "date"; @Prop() value?: string; @Prop() defaultValue?: string; @Prop() min?: string; @Prop() max?: string; @Prop() step?: number; @Prop() disabled?: boolean; @Prop() readonly?: boolean; @Prop() required?: boolean; @Prop() invalid?: boolean; @Prop() name?: string; @Prop() class?: string; @Prop() id?: string; @Prop() "aria-label"?: string; @FunctionProp() onChange?: TemporalInputProps["onChange"]; private readonly _handleChange = (event: Event) => { this.onChange?.((event.target as HTMLInputElement).value); }; render() { return ( <input id={this.id} type={this.type} name={this.name} value={() => this.value ?? this.defaultValue ?? ""} min={() => this.min} max={() => this.max} step={this.step} disabled={this.disabled} readOnly={this.readonly} required={this.required} aria-label={this["aria-label"]} aria-invalid={this.invalid ? ("true" as const) : undefined} onChange={this._handleChange} class={cn( "flex h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs outline-none transition-[color,box-shadow] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 focus:border-ring focus:ring-[3px] focus:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm", "[&::-webkit-calendar-picker-indicator]:cursor-pointer [&::-webkit-calendar-picker-indicator]:opacity-60 [&::-webkit-calendar-picker-indicator:hover]:opacity-100 dark:[&::-webkit-calendar-picker-indicator]:invert", this.class, )} /> ); }}export type DateInputProps = Omit<TemporalInputProps, "type">;@Component()export class DateInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="date" {...this.props} />; }}@Component()export class TimeInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="time" {...this.props} />; }}@Component()export class DateTimeInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="datetime-local" {...this.props} />; }}export interface TemporalRange { from?: string; to?: string;}export interface TemporalRangeInputProps { type: TemporalInputType; value?: TemporalRange; defaultValue?: TemporalRange; min?: string; max?: string; step?: number; onChange?: (range: TemporalRange) => void; disabled?: boolean; readonly?: boolean; required?: boolean; invalid?: boolean; class?: string; id?: string;}@Component()export class TemporalRangeInput extends StatefulComponent { @Prop() type: TemporalInputType = "date"; @Prop() value?: TemporalRange; @Prop() defaultValue?: TemporalRange; @Prop() min?: string; @Prop() max?: string; @Prop() step?: number; @Prop() disabled?: boolean; @Prop() readonly?: boolean; @Prop() required?: boolean; @Prop() invalid?: boolean; @Prop() class?: string; @Prop() id?: string; @FunctionProp() onChange?: TemporalRangeInputProps["onChange"]; @State() _range: TemporalRange = {}; onBeforeMount() { this._range = this.defaultValue ?? {}; } get range(): TemporalRange { return this.value ?? this._range; } private _update(patch: TemporalRange): void { const next = { ...this.range, ...patch }; if (this.value === undefined) this._range = next; this.onChange?.(next); } render() { return ( <div id={this.id} role="group" class={cn("flex w-full items-center gap-2", this.class)}> <TemporalInput type={this.type} aria-label="Start" value={() => this.range.from ?? ""} min={() => this.min} max={() => this.range.to ?? this.max} step={this.step} disabled={this.disabled} readonly={this.readonly} required={this.required} invalid={this.invalid} onChange={(from: string) => { this._update({ from: from === "" ? undefined : from }); }} /> <span aria-hidden class="text-sm text-muted-foreground">–</span> <TemporalInput type={this.type} aria-label="End" value={() => this.range.to ?? ""} min={() => this.range.from ?? this.min} max={() => this.max} step={this.step} disabled={this.disabled} readonly={this.readonly} required={this.required} invalid={this.invalid} onChange={(to: string) => { this._update({ to: to === "" ? undefined : to }); }} /> </div> ); }}export type DateRangeInputProps = Omit<TemporalRangeInputProps, "type">;@Component()export class DateRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="date" {...this.props} />; }}@Component()export class TimeRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="time" {...this.props} />; }}@Component()export class DateTimeRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="datetime-local" {...this.props} />; }}Install the following dependencies:
npm install @praxisjs/csspnpm add @praxisjs/cssyarn add @praxisjs/cssbun add @praxisjs/cssCopy and paste the following code into your project.
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, FunctionProp, Prop, State } from "@praxisjs/decorators";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class DateTimeInputStyles extends Stylesheet { $root = this.css({ display: "flex", height: "2.25rem", width: "100%", minWidth: "0", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.input}`, backgroundColor: "transparent", padding: "0.25rem 0.75rem", fontSize: "0.875rem", boxShadow: "0 1px 2px 0 rgb(0 0 0 / 0.05)", outline: "none", transition: "color 120ms ease, box-shadow 120ms ease", }) .disabled({ pointerEvents: "none", cursor: "not-allowed", opacity: 0.5 }) .focus({ borderColor: t.ring, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.ring} 50%, transparent)` }) .on("&[aria-invalid]", { borderColor: t.destructive, boxShadow: `0 0 0 3px color-mix(in oklab, ${t.destructive} 20%, transparent)` }) .on("&::-webkit-calendar-picker-indicator", { cursor: "pointer", opacity: 0.6 }) .on("&::-webkit-calendar-picker-indicator:hover", { opacity: 1 }); $range = this.css({ display: "flex", width: "100%", alignItems: "center", gap: "0.5rem" }); $separator = this.css({ fontSize: "0.875rem", color: t.mutedForeground });}export type TemporalInputType = "date" | "time" | "datetime-local";export interface TemporalInputProps { type: TemporalInputType; /** `"YYYY-MM-DD"`, `"HH:mm[:ss]"` or `"YYYY-MM-DDTHH:mm"`, matching the native input's value format. */ value?: string; defaultValue?: string; min?: string; max?: string; step?: number; onChange?: (value: string) => void; disabled?: boolean; readonly?: boolean; required?: boolean; invalid?: boolean; name?: string; class?: string; id?: string; "aria-label"?: string;}@Component()export class TemporalInput extends StatefulComponent { @Styled(DateTimeInputStyles) $s!: DateTimeInputStyles; @Prop() type: TemporalInputType = "date"; @Prop() value?: string; @Prop() defaultValue?: string; @Prop() min?: string; @Prop() max?: string; @Prop() step?: number; @Prop() disabled?: boolean; @Prop() readonly?: boolean; @Prop() required?: boolean; @Prop() invalid?: boolean; @Prop() name?: string; @Prop() class?: string; @Prop() id?: string; @Prop() "aria-label"?: string; @FunctionProp() onChange?: TemporalInputProps["onChange"]; private readonly _handleChange = (event: Event) => { this.onChange?.((event.target as HTMLInputElement).value); }; render() { return ( <input id={this.id} type={this.type} name={this.name} value={() => this.value ?? this.defaultValue ?? ""} min={() => this.min} max={() => this.max} step={this.step} disabled={this.disabled} readOnly={this.readonly} required={this.required} aria-label={this["aria-label"]} aria-invalid={this.invalid ? ("true" as const) : undefined} onChange={this._handleChange} class={cx(this.$s.$root, this.class)} /> ); }}export type DateInputProps = Omit<TemporalInputProps, "type">;@Component()export class DateInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="date" {...this.props} />; }}@Component()export class TimeInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="time" {...this.props} />; }}@Component()export class DateTimeInput extends StatelessComponent<DateInputProps> { render() { return <TemporalInput type="datetime-local" {...this.props} />; }}export interface TemporalRange { from?: string; to?: string;}export interface TemporalRangeInputProps { type: TemporalInputType; value?: TemporalRange; defaultValue?: TemporalRange; min?: string; max?: string; step?: number; onChange?: (range: TemporalRange) => void; disabled?: boolean; readonly?: boolean; required?: boolean; invalid?: boolean; class?: string; id?: string;}@Component()export class TemporalRangeInput extends StatefulComponent { @Styled(DateTimeInputStyles) $s!: DateTimeInputStyles; @Prop() type: TemporalInputType = "date"; @Prop() value?: TemporalRange; @Prop() defaultValue?: TemporalRange; @Prop() min?: string; @Prop() max?: string; @Prop() step?: number; @Prop() disabled?: boolean; @Prop() readonly?: boolean; @Prop() required?: boolean; @Prop() invalid?: boolean; @Prop() class?: string; @Prop() id?: string; @FunctionProp() onChange?: TemporalRangeInputProps["onChange"]; @State() _range: TemporalRange = {}; onBeforeMount() { this._range = this.defaultValue ?? {}; } get range(): TemporalRange { return this.value ?? this._range; } private _update(patch: TemporalRange): void { const next = { ...this.range, ...patch }; if (this.value === undefined) this._range = next; this.onChange?.(next); } render() { return ( <div id={this.id} role="group" class={cx(this.$s.$range, this.class)}> <TemporalInput type={this.type} aria-label="Start" value={() => this.range.from ?? ""} min={() => this.min} max={() => this.range.to ?? this.max} step={this.step} disabled={this.disabled} readonly={this.readonly} required={this.required} invalid={this.invalid} onChange={(from: string) => { this._update({ from: from === "" ? undefined : from }); }} /> <span aria-hidden class={this.$s.$separator}>–</span> <TemporalInput type={this.type} aria-label="End" value={() => this.range.to ?? ""} min={() => this.range.from ?? this.min} max={() => this.max} step={this.step} disabled={this.disabled} readonly={this.readonly} required={this.required} invalid={this.invalid} onChange={(to: string) => { this._update({ to: to === "" ? undefined : to }); }} /> </div> ); }}export type DateRangeInputProps = Omit<TemporalRangeInputProps, "type">;@Component()export class DateRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="date" {...this.props} />; }}@Component()export class TimeRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="time" {...this.props} />; }}@Component()export class DateTimeRangeInput extends StatelessComponent<DateRangeInputProps> { render() { return <TemporalRangeInput type="datetime-local" {...this.props} />; }}Examples
Usage
import { DateInput, DateRangeInput, TimeInput } from "@/components/ui/date-time-input";
<DateInput defaultValue="2026-07-15" onChange={(value) => console.log(value)} />
<TimeInput min="09:00" max="18:00" />
<DateRangeInput onChange={({ from, to }) => console.log(from, to)} />These wrap the browser's native date, time and datetime-local inputs, so values use the native
string formats (YYYY-MM-DD, HH:mm, YYYY-MM-DDTHH:mm) and the platform supplies the keyboard
editing, localization and picker. Use the pickers when you want
the Kosmesis calendar and time columns instead.
Range variants render a start and an end field and keep them consistent: the start's max follows the
end value and the end's min follows the start.
Props
Exports: DateInput, TimeInput, DateTimeInput and DateRangeInput, TimeRangeInput,
DateTimeRangeInput.
DateInput / TimeInput / DateTimeInput
| Prop | Type | Default |
|---|---|---|
value | string | — (controlled) |
defaultValue | string | — |
min / max | string | — |
step | number | — |
onChange | (value: string) => void | — |
disabled / readonly / required / invalid | boolean | — |
name / id / class / aria-label | string | — |
DateRangeInput / TimeRangeInput / DateTimeRangeInput
| Prop | Type | Default |
|---|---|---|
value | { from?: string; to?: string } | — (controlled) |
defaultValue | { from?: string; to?: string } | — |
min / max | string | — |
step | number | — |
onChange | (range: { from?: string; to?: string }) => void | — |
disabled / readonly / required / invalid | boolean | — |
id / class | string | — |