Kosmesis
Components

Date Time Picker

A Popover + Calendar + time columns composition for picking a date and time, or a date-time range.

Installation

npx kosmesis add date-time-picker
pnpm dlx kosmesis add date-time-picker
yarn dlx kosmesis add date-time-picker
bunx kosmesis add date-time-picker

Install the following dependencies:

npm install @morphos/overlays @morphos/icons
pnpm add @morphos/overlays @morphos/icons
yarn add @morphos/overlays @morphos/icons
bun add @morphos/overlays @morphos/icons

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button, calendar, popover, time-picker.

Copy and paste the following code into your project.

date-time-picker.tsx
import { StatelessComponent } from "@praxisjs/core";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { type Popover, PopoverTrigger } from "@morphos/overlays";import { buttonVariants } from "./button";import { Calendar, type CalendarState } from "./calendar";import { PopoverContent } from "./popover";import { TimePickerPanel, type TimePickerState } from "./time-picker";import { cn } from "@/lib/utils";export interface DateTimePickerProps {  popover: Popover;  calendar: CalendarState;  time: TimePickerState;  placeholder?: string;  class?: string;}@Component()export class DateTimePicker extends StatelessComponent<DateTimePickerProps> {  render() {    const { popover, calendar, time, placeholder = "Pick date and time", class: cls } = this.props;    return (      <>        <PopoverTrigger popover={popover} class={cn(buttonVariants({ variant: "outline" }), "justify-start font-normal", cls)}>          <Icon name="Calendar" size={14} />          {() => {            const date = calendar.formattedDate;            if (!date) return <span class="text-muted-foreground">{placeholder}</span>;            return time.formattedValue ? `${date}, ${time.formattedValue}` : date;          }}        </PopoverTrigger>        <PopoverContent popover={popover} class="flex w-auto p-0">          <Calendar state={calendar} class="border-none shadow-none" />          <TimePickerPanel state={time} class="rounded-none border-0 border-l" />        </PopoverContent>      </>    );  }}export interface DateTimeRangePickerProps {  popover: Popover;  /** Must be created with `mode: "range"`. */  calendar: CalendarState;  startTime: TimePickerState;  endTime: TimePickerState;  placeholder?: string;  class?: string;}@Component()export class DateTimeRangePicker extends StatelessComponent<DateTimeRangePickerProps> {  render() {    const { popover, calendar, startTime, endTime, placeholder = "Pick a date and time range", class: cls } = this.props;    return (      <>        <PopoverTrigger popover={popover} class={cn(buttonVariants({ variant: "outline" }), "justify-start font-normal", cls)}>          <Icon name="Calendar" size={14} />          {() => {            const { from, to } = calendar.range;            if (!from) return <span class="text-muted-foreground">{placeholder}</span>;            const start = [from.toLocaleDateString(), startTime.formattedValue].filter(Boolean).join(", ");            const end = to ? [to.toLocaleDateString(), endTime.formattedValue].filter(Boolean).join(", ") : "…";            return `${start} – ${end}`;          }}        </PopoverTrigger>        <PopoverContent popover={popover} class="flex w-auto p-0">          <Calendar state={calendar} class="border-none shadow-none" />          <div class="flex flex-col gap-1 border-l p-2">            <span class="px-1 text-xs font-medium text-muted-foreground">Start</span>            <TimePickerPanel state={startTime} class="border-0 p-0" />          </div>          <div class="flex flex-col gap-1 border-l p-2">            <span class="px-1 text-xs font-medium text-muted-foreground">End</span>            <TimePickerPanel state={endTime} class="border-0 p-0" />          </div>        </PopoverContent>      </>    );  }}

Install the following dependencies:

npm install @morphos/overlays @morphos/icons @praxisjs/css
pnpm add @morphos/overlays @morphos/icons @praxisjs/css
yarn add @morphos/overlays @morphos/icons @praxisjs/css
bun add @morphos/overlays @morphos/icons @praxisjs/css

This component builds on other Kosmesis components — install these first (via kosmesis add or this same manual process on their own pages): button, calendar, popover, time-picker.

Copy and paste the following code into your project.

date-time-picker.tsx
import { StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component } from "@praxisjs/decorators";import { Icon } from "@morphos/icons";import { type Popover, PopoverTrigger } from "@morphos/overlays";import { ButtonStyles } from "./button";import { Calendar, type CalendarState } from "./calendar";import { PopoverContent } from "./popover";import { TimePickerPanel, type TimePickerState } from "./time-picker";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);class DateTimePickerStyles extends Stylesheet {  $trigger = this.css({ justifyContent: "flex-start", fontWeight: 400 });  $placeholder = this.css({ color: t.mutedForeground });  $content = this.css({ display: "flex", width: "auto", padding: "0" });  $flush = this.css({ border: "none !important" });  $timeFlush = this.css({ border: "none !important", borderRadius: "0", borderLeft: `1px solid ${t.border} !important` });  $section = this.css({ display: "flex", flexDirection: "column", gap: "0.25rem", padding: "0.5rem", borderLeft: `1px solid ${t.border}` });  $sectionLabel = this.css({ padding: "0 0.25rem", fontSize: "0.75rem", fontWeight: 500, color: t.mutedForeground });  $sectionPanel = this.css({ border: "none !important", padding: "0" });}export interface DateTimePickerProps {  popover: Popover;  calendar: CalendarState;  time: TimePickerState;  placeholder?: string;  class?: string;}@Component()export class DateTimePicker extends StatelessComponent<DateTimePickerProps> {  @Styled(ButtonStyles) $btn!: ButtonStyles;  @Styled(DateTimePickerStyles) $s!: DateTimePickerStyles;  render() {    const { popover, calendar, time, placeholder = "Pick date and time", class: cls } = this.props;    return (      <>        <PopoverTrigger          popover={popover}          class={cx(this.$btn.$root, this.$btn.$variantOutline, this.$btn.$sizeDefault, this.$s.$trigger, cls)}        >          <Icon name="Calendar" size={14} />          {() => {            const date = calendar.formattedDate;            if (!date) return <span class={this.$s.$placeholder}>{placeholder}</span>;            return time.formattedValue ? `${date}, ${time.formattedValue}` : date;          }}        </PopoverTrigger>        <PopoverContent popover={popover} class={this.$s.$content}>          <Calendar state={calendar} class={this.$s.$flush} />          <TimePickerPanel state={time} class={this.$s.$timeFlush} />        </PopoverContent>      </>    );  }}export interface DateTimeRangePickerProps {  popover: Popover;  /** Must be created with `mode: "range"`. */  calendar: CalendarState;  startTime: TimePickerState;  endTime: TimePickerState;  placeholder?: string;  class?: string;}@Component()export class DateTimeRangePicker extends StatelessComponent<DateTimeRangePickerProps> {  @Styled(ButtonStyles) $btn!: ButtonStyles;  @Styled(DateTimePickerStyles) $s!: DateTimePickerStyles;  render() {    const { popover, calendar, startTime, endTime, placeholder = "Pick a date and time range", class: cls } = this.props;    return (      <>        <PopoverTrigger          popover={popover}          class={cx(this.$btn.$root, this.$btn.$variantOutline, this.$btn.$sizeDefault, this.$s.$trigger, cls)}        >          <Icon name="Calendar" size={14} />          {() => {            const { from, to } = calendar.range;            if (!from) return <span class={this.$s.$placeholder}>{placeholder}</span>;            const start = [from.toLocaleDateString(), startTime.formattedValue].filter(Boolean).join(", ");            const end = to ? [to.toLocaleDateString(), endTime.formattedValue].filter(Boolean).join(", ") : "…";            return `${start} – ${end}`;          }}        </PopoverTrigger>        <PopoverContent popover={popover} class={this.$s.$content}>          <Calendar state={calendar} class={this.$s.$flush} />          <div class={this.$s.$section}>            <span class={this.$s.$sectionLabel}>Start</span>            <TimePickerPanel state={startTime} class={this.$s.$sectionPanel} />          </div>          <div class={this.$s.$section}>            <span class={this.$s.$sectionLabel}>End</span>            <TimePickerPanel state={endTime} class={this.$s.$sectionPanel} />          </div>        </PopoverContent>      </>    );  }}

Examples

Usage

import { Popover } from "@morphos/overlays";
import { CalendarState } from "@/components/ui/calendar";
import { DateTimePicker } from "@/components/ui/date-time-picker";
import { TimePickerState } from "@/components/ui/time-picker";

@Component()
class EventForm extends StatefulComponent {
  @State() popover = new Popover();
  @State() calendar = new CalendarState();
  @State() time = new TimePickerState({ minuteStep: 5 });

  onBeforeMount() {
    this.popover.onBeforeMount();
    this.calendar.onBeforeMount();
    this.time.onBeforeMount();
  }

  render() {
    return <DateTimePicker popover={this.popover} calendar={this.calendar} time={this.time} />;
  }
}

For a range, create the calendar with mode: "range" and use DateTimeRangePicker with a startTime and an endTime state. Unlike Date Picker, the popover stays open after a selection, since both a date and a time need to be chosen.

Props

DateTimePicker

PropTypeDefault
popoverPopover— required
calendarCalendarState— required
timeTimePickerState— required
placeholderstring"Pick date and time"
classstring—

DateTimeRangePicker

PropTypeDefault
popoverPopover— required
calendarCalendarState— required, mode: "range"
startTimeTimePickerState— required
endTimeTimePickerState— required
placeholderstring"Pick a date and time range"
classstring—

On this page