Kosmesis
Components

Calendar

A month-grid date picker (single date or range) with real date math. Purely presentational — no Morphos equivalent.

Installation

npx kosmesis add calendar
pnpm dlx kosmesis add calendar
yarn dlx kosmesis add calendar
bunx kosmesis add calendar

Install the following dependencies:

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

Copy and paste the following code into your project.

calendar.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { Component, Emit, FunctionProp, Prop, State } from "@praxisjs/decorators";import { For } from "@praxisjs/runtime";import { Icon } from "@morphos/icons";import { cn } from "@/lib/utils";const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];const MONTH_LABELS = [  "January", "February", "March", "April", "May", "June",  "July", "August", "September", "October", "November", "December",];function isSameDay(a: Date, b: Date): boolean {  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();}function compareDays(a: Date, b: Date): number {  return (    new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime() -    new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime()  );}function startOfMonth(year: number, month: number): Date {  return new Date(year, month, 1);}// Always 6 rows (42 cells), including leading/trailing days of adjacent months.function buildMonthGrid(year: number, month: number): Array<{ date: Date; inMonth: boolean }> {  const first = startOfMonth(year, month);  const gridStart = new Date(year, month, 1 - first.getDay());  return Array.from({ length: 42 }, (_, i) => {    const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);    return { date, inMonth: date.getMonth() === month };  });}export interface DateRange {  from?: Date;  to?: Date;}export interface CalendarStateProps {  defaultMonth?: Date;  mode?: "single" | "range";  selected?: Date;  selectedRange?: DateRange;  onSelect?: (date: Date) => void;  onSelectRange?: (range: DateRange) => void;  disabled?: (date: Date) => boolean;}@Component()export class CalendarState extends StatefulComponent {  @Prop() defaultMonth?: Date;  @Prop() mode: "single" | "range" = "single";  @Prop() selected?: Date;  @Prop() selectedRange?: DateRange;  // `@FunctionProp()` (not `@Prop()`) — `onSelect`/`disabled` are real callbacks, and `@Prop()`  // would auto-invoke a function value instead of passing it through.  @FunctionProp() onSelect?: CalendarStateProps["onSelect"];  @FunctionProp() onSelectRange?: CalendarStateProps["onSelectRange"];  @FunctionProp() disabled?: CalendarStateProps["disabled"];  @State() _viewYear = 0;  @State() _viewMonth = 0;  @State() _selected: Date | undefined = undefined;  @State() _range: DateRange = {};  onBeforeMount() {    const base = this.defaultMonth ?? this.selected ?? this.selectedRange?.from ?? new Date();    this._viewYear = base.getFullYear();    this._viewMonth = base.getMonth();    this._selected = this.selected;    this._range = this.selectedRange ?? {};  }  get monthLabel(): string {    return `${MONTH_LABELS[this._viewMonth]} ${String(this._viewYear)}`;  }  get weekdayLabels(): string[] {    return WEEKDAY_LABELS;  }  get grid(): Array<{ date: Date; inMonth: boolean }> {    return buildMonthGrid(this._viewYear, this._viewMonth);  }  get selectedDate(): Date | undefined {    return this.selected ?? this._selected;  }  get range(): DateRange {    return this.selectedRange ?? this._range;  }  get formattedDate(): string | undefined {    return this.selectedDate?.toLocaleDateString();  }  get formattedRange(): string | undefined {    const { from, to } = this.range;    if (!from) return undefined;    return `${from.toLocaleDateString()} – ${to ? to.toLocaleDateString() : "…"}`;  }  isSelected(date: Date): boolean {    if (this.mode === "range") return this.isRangeStart(date) || this.isRangeEnd(date);    const selected = this.selectedDate;    return selected !== undefined && isSameDay(date, selected);  }  isRangeStart(date: Date): boolean {    const { from } = this.range;    return from !== undefined && isSameDay(date, from);  }  isRangeEnd(date: Date): boolean {    const { to } = this.range;    return to !== undefined && isSameDay(date, to);  }  isInRange(date: Date): boolean {    const { from, to } = this.range;    return from !== undefined && to !== undefined && compareDays(date, from) > 0 && compareDays(date, to) < 0;  }  isToday(date: Date): boolean {    return isSameDay(date, new Date());  }  isDisabled(date: Date): boolean {    return this.disabled?.(date) ?? false;  }  goToPrevMonth(): void {    const prev = new Date(this._viewYear, this._viewMonth - 1, 1);    this._viewYear = prev.getFullYear();    this._viewMonth = prev.getMonth();  }  goToNextMonth(): void {    const next = new Date(this._viewYear, this._viewMonth + 1, 1);    this._viewYear = next.getFullYear();    this._viewMonth = next.getMonth();  }  @Emit("onSelect")  select(date: Date): Date {    if (this.isDisabled(date)) return this.selectedDate ?? date;    if (this.selected === undefined) this._selected = date;    return date;  }  @Emit("onSelectRange")  selectRange(date: Date): DateRange {    if (this.isDisabled(date)) return this.range;    const { from, to } = this.range;    const next: DateRange = !from || to || compareDays(date, from) < 0 ? { from: date } : { from, to: date };    if (this.selectedRange === undefined) this._range = next;    return next;  }  pick(date: Date): void {    if (this.mode === "range") this.selectRange(date);    else this.select(date);  }  // Never mounted via JSX — only instantiated directly.  render() {    return null;  }}export interface CalendarProps {  state: CalendarState;  class?: string;}@Component()export class Calendar extends StatelessComponent<CalendarProps> {  render() {    const { state, class: cls } = this.props;    return (      <div class={cn("w-fit rounded-md border bg-background p-3", cls)}>        <div class="mb-4 flex items-center justify-between">          <button            type="button"            aria-label="Previous month"            class="inline-flex size-7 items-center justify-center rounded-md border hover:bg-accent hover:text-accent-foreground"            onClick={() => { state.goToPrevMonth(); }}          >            <Icon name="ChevronLeft" size={14} />          </button>          <span class="text-sm font-medium">{() => state.monthLabel}</span>          <button            type="button"            aria-label="Next month"            class="inline-flex size-7 items-center justify-center rounded-md border hover:bg-accent hover:text-accent-foreground"            onClick={() => { state.goToNextMonth(); }}          >            <Icon name="ChevronRight" size={14} />          </button>        </div>        <div class="grid grid-cols-7 gap-1 text-center text-xs text-muted-foreground">          {state.weekdayLabels.map((label) => (            <div key={label} class="flex h-8 items-center justify-center font-normal">              {label}            </div>          ))}        </div>        <div class="grid grid-cols-7 gap-1">          <For each={() => state.grid} by={(cell) => cell.date.toISOString()}>            {(cell) => {              const { date } = cell();              return (                <button                  type="button"                  disabled={() => state.isDisabled(date)}                  data-selected={() => (state.isSelected(date) ? "" : undefined)}                  data-today={() => (state.isToday(date) ? "" : undefined)}                  data-range-middle={() => (state.isInRange(date) ? "" : undefined)}                  data-outside-month={() => (!cell().inMonth ? "" : undefined)}                  class={cn(                    "flex size-8 items-center justify-center rounded-md p-0 text-sm font-normal text-foreground",                    "hover:bg-accent hover:text-accent-foreground",                    "data-outside-month:text-muted-foreground data-outside-month:opacity-50",                    "data-selected:bg-primary data-selected:text-primary-foreground data-selected:hover:bg-primary data-selected:hover:text-primary-foreground",                    "data-range-middle:bg-accent data-range-middle:text-accent-foreground",                    "data-today:border data-today:border-input",                    "disabled:pointer-events-none disabled:opacity-30",                  )}                  onClick={() => { state.pick(date); }}                >                  {date.getDate()}                </button>              );            }}          </For>        </div>      </div>    );  }}

Install the following dependencies:

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

Copy and paste the following code into your project.

calendar.tsx
import { StatefulComponent, StatelessComponent } from "@praxisjs/core";import { cx, Stylesheet, Styled, tokenVars } from "@praxisjs/css";import { Component, Emit, FunctionProp, Prop, State } from "@praxisjs/decorators";import { For } from "@praxisjs/runtime";import { Icon } from "@morphos/icons";import { KosmesisTokens } from "@/lib/kosmesis-theme";const t = tokenVars(KosmesisTokens);const WEEKDAY_LABELS = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];const MONTH_LABELS = [  "January", "February", "March", "April", "May", "June",  "July", "August", "September", "October", "November", "December",];function isSameDay(a: Date, b: Date): boolean {  return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();}function compareDays(a: Date, b: Date): number {  return (    new Date(a.getFullYear(), a.getMonth(), a.getDate()).getTime() -    new Date(b.getFullYear(), b.getMonth(), b.getDate()).getTime()  );}function startOfMonth(year: number, month: number): Date {  return new Date(year, month, 1);}// Always 6 rows (42 cells), including leading/trailing days of adjacent months.function buildMonthGrid(year: number, month: number): Array<{ date: Date; inMonth: boolean }> {  const first = startOfMonth(year, month);  const gridStart = new Date(year, month, 1 - first.getDay());  return Array.from({ length: 42 }, (_, i) => {    const date = new Date(gridStart.getFullYear(), gridStart.getMonth(), gridStart.getDate() + i);    return { date, inMonth: date.getMonth() === month };  });}export interface DateRange {  from?: Date;  to?: Date;}export interface CalendarStateProps {  defaultMonth?: Date;  mode?: "single" | "range";  selected?: Date;  selectedRange?: DateRange;  onSelect?: (date: Date) => void;  onSelectRange?: (range: DateRange) => void;  disabled?: (date: Date) => boolean;}@Component()export class CalendarState extends StatefulComponent {  @Prop() defaultMonth?: Date;  @Prop() mode: "single" | "range" = "single";  @Prop() selected?: Date;  @Prop() selectedRange?: DateRange;  // `@FunctionProp()` (not `@Prop()`) — `onSelect`/`disabled` are real callbacks, and `@Prop()`  // would auto-invoke a function value instead of passing it through.  @FunctionProp() onSelect?: CalendarStateProps["onSelect"];  @FunctionProp() onSelectRange?: CalendarStateProps["onSelectRange"];  @FunctionProp() disabled?: CalendarStateProps["disabled"];  @State() _viewYear = 0;  @State() _viewMonth = 0;  @State() _selected: Date | undefined = undefined;  @State() _range: DateRange = {};  onBeforeMount() {    const base = this.defaultMonth ?? this.selected ?? this.selectedRange?.from ?? new Date();    this._viewYear = base.getFullYear();    this._viewMonth = base.getMonth();    this._selected = this.selected;    this._range = this.selectedRange ?? {};  }  get monthLabel(): string {    return `${MONTH_LABELS[this._viewMonth]} ${String(this._viewYear)}`;  }  get weekdayLabels(): string[] {    return WEEKDAY_LABELS;  }  get grid(): Array<{ date: Date; inMonth: boolean }> {    return buildMonthGrid(this._viewYear, this._viewMonth);  }  get selectedDate(): Date | undefined {    return this.selected ?? this._selected;  }  get range(): DateRange {    return this.selectedRange ?? this._range;  }  get formattedDate(): string | undefined {    return this.selectedDate?.toLocaleDateString();  }  get formattedRange(): string | undefined {    const { from, to } = this.range;    if (!from) return undefined;    return `${from.toLocaleDateString()} – ${to ? to.toLocaleDateString() : "…"}`;  }  isSelected(date: Date): boolean {    if (this.mode === "range") return this.isRangeStart(date) || this.isRangeEnd(date);    const selected = this.selectedDate;    return selected !== undefined && isSameDay(date, selected);  }  isRangeStart(date: Date): boolean {    const { from } = this.range;    return from !== undefined && isSameDay(date, from);  }  isRangeEnd(date: Date): boolean {    const { to } = this.range;    return to !== undefined && isSameDay(date, to);  }  isInRange(date: Date): boolean {    const { from, to } = this.range;    return from !== undefined && to !== undefined && compareDays(date, from) > 0 && compareDays(date, to) < 0;  }  isToday(date: Date): boolean {    return isSameDay(date, new Date());  }  isDisabled(date: Date): boolean {    return this.disabled?.(date) ?? false;  }  goToPrevMonth(): void {    const prev = new Date(this._viewYear, this._viewMonth - 1, 1);    this._viewYear = prev.getFullYear();    this._viewMonth = prev.getMonth();  }  goToNextMonth(): void {    const next = new Date(this._viewYear, this._viewMonth + 1, 1);    this._viewYear = next.getFullYear();    this._viewMonth = next.getMonth();  }  @Emit("onSelect")  select(date: Date): Date {    if (this.isDisabled(date)) return this.selectedDate ?? date;    if (this.selected === undefined) this._selected = date;    return date;  }  @Emit("onSelectRange")  selectRange(date: Date): DateRange {    if (this.isDisabled(date)) return this.range;    const { from, to } = this.range;    const next: DateRange = !from || to || compareDays(date, from) < 0 ? { from: date } : { from, to: date };    if (this.selectedRange === undefined) this._range = next;    return next;  }  pick(date: Date): void {    if (this.mode === "range") this.selectRange(date);    else this.select(date);  }  // Never mounted via JSX — only instantiated directly.  render() {    return null;  }}class CalendarStyles extends Stylesheet {  $root = this.css({ width: "fit-content", borderRadius: `calc(${t.radius} - 2px)`, border: `1px solid ${t.border}`, backgroundColor: t.background, padding: "0.75rem" });  $navHeader = this.css({ marginBottom: "1rem", display: "flex", alignItems: "center", justifyContent: "space-between" });  $navButton = this.css({    display: "inline-flex",    width: "1.75rem",    height: "1.75rem",    alignItems: "center",    justifyContent: "center",    borderRadius: `calc(${t.radius} - 2px)`,    border: `1px solid ${t.border}`,  }).hover({ backgroundColor: t.accent, color: t.accentForeground });  $monthLabel = this.css({ fontSize: "0.875rem", fontWeight: 500 });  $weekdayRow = this.css({ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "0.25rem", textAlign: "center", fontSize: "0.75rem", color: t.mutedForeground });  $weekday = this.css({ display: "flex", height: "2rem", alignItems: "center", justifyContent: "center", fontWeight: 400 });  $grid = this.css({ display: "grid", gridTemplateColumns: "repeat(7, 1fr)", gap: "0.25rem" });  $day = this.css({    display: "flex",    width: "2rem",    height: "2rem",    alignItems: "center",    justifyContent: "center",    borderRadius: `calc(${t.radius} - 2px)`,    padding: "0",    fontSize: "0.875rem",    fontWeight: 400,    color: t.foreground,  })    .hover({ backgroundColor: t.accent, color: t.accentForeground })    .on("&[data-outside-month]", { color: t.mutedForeground, opacity: 0.5 })    .on("&[data-selected]", { backgroundColor: t.primary, color: t.primaryForeground })    .on("&[data-selected]:hover", { backgroundColor: t.primary, color: t.primaryForeground })    .on("&[data-range-middle]", { backgroundColor: t.accent, color: t.accentForeground })    .on("&[data-today]", { border: `1px solid ${t.input}` })    .disabled({ pointerEvents: "none", opacity: 0.3 });}export interface CalendarProps {  state: CalendarState;  class?: string;}@Component()export class Calendar extends StatelessComponent<CalendarProps> {  @Styled(CalendarStyles) $s!: CalendarStyles;  render() {    const { state, class: cls } = this.props;    return (      <div class={cx(this.$s.$root, cls)}>        <div class={this.$s.$navHeader}>          <button type="button" aria-label="Previous month" class={this.$s.$navButton} onClick={() => { state.goToPrevMonth(); }}>            <Icon name="ChevronLeft" size={14} />          </button>          <span class={this.$s.$monthLabel}>{() => state.monthLabel}</span>          <button type="button" aria-label="Next month" class={this.$s.$navButton} onClick={() => { state.goToNextMonth(); }}>            <Icon name="ChevronRight" size={14} />          </button>        </div>        <div class={this.$s.$weekdayRow}>          {state.weekdayLabels.map((label) => (            <div key={label} class={this.$s.$weekday}>              {label}            </div>          ))}        </div>        <div class={this.$s.$grid}>          <For each={() => state.grid} by={(cell) => cell.date.toISOString()}>            {(cell) => {              const { date } = cell();              return (                <button                  type="button"                  disabled={() => state.isDisabled(date)}                  data-selected={() => (state.isSelected(date) ? "" : undefined)}                  data-today={() => (state.isToday(date) ? "" : undefined)}                  data-range-middle={() => (state.isInRange(date) ? "" : undefined)}                  data-outside-month={() => (!cell().inMonth ? "" : undefined)}                  class={this.$s.$day}                  onClick={() => { state.pick(date); }}                >                  {date.getDate()}                </button>              );            }}          </For>        </div>      </div>    );  }}

Examples

Usage

import { Calendar, CalendarState } from "@/components/ui/calendar";

@Component()
class BookingCalendar extends StatefulComponent {
  @State() calendarState = new CalendarState({
    onSelect: (date) => console.log(date),
  });

  render() {
    return <Calendar state={this.calendarState} />;
  }
}

Props

CalendarState

PropTypeDefault
defaultMonthDatetoday
mode"single" | "range""single"
selectedDate— (controlled, single mode)
selectedRange{ from?: Date; to?: Date }— (controlled, range mode)
onSelect(date: Date) => void— (single mode)
onSelectRange(range: { from?: Date; to?: Date }) => void— (range mode)
disabled(date: Date) => boolean—

Calendar

PropTypeDefault
stateCalendarState— required

In range mode the first click sets from, the second sets to; clicking before from, or after a completed range, starts a new range.

@State() calendarState = new CalendarState({
  mode: "range",
  onSelectRange: ({ from, to }) => console.log(from, to),
});

For a Popover-triggered variant, see Date Picker.

On this page