Kosmesis
Components

Calendar

A month-grid date picker 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

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 { 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 startOfMonth(year: number, month: number): Date {  return new Date(year, month, 1);}/** Builds a 6-row (42-cell) month grid, including the trailing/leading 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 CalendarStateProps {  defaultMonth?: Date;  selected?: Date;  onSelect?: (date: Date) => void;  disabled?: (date: Date) => boolean;}/** * Purely presentational + date math — no Morphos equivalent (Radix has no calendar primitive * either). `CalendarState` owns the visible month and selection; `Calendar` renders the grid it * computes. Both instantiated directly + rendered, the same pattern as every other compound * component here. */@Component()export class CalendarState extends StatefulComponent {  @Prop() defaultMonth?: Date;  @Prop() selected?: Date;  // `@Prop()`'s getter auto-invokes any raw value that is itself a function, treating it as a  // reactive `() => T` binding (see `selected` in the Calendar stories, which relies on exactly  // that to stay controlled). `onSelect`/`disabled` are real callbacks, not value-returning  // thunks — `@FunctionProp()` returns them as-is instead of calling them.  @FunctionProp() onSelect?: CalendarStateProps["onSelect"];  @FunctionProp() disabled?: CalendarStateProps["disabled"];  @State() _viewYear = 0;  @State() _viewMonth = 0;  @State() _selected: Date | undefined = undefined;  onBeforeMount() {    const base = this.defaultMonth ?? this.selected ?? new Date();    this._viewYear = base.getFullYear();    this._viewMonth = base.getMonth();    this._selected = this.selected;  }  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;  }  isSelected(date: Date): boolean {    const selected = this.selectedDate;    return selected !== undefined && isSameDay(date, selected);  }  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;  }  /** Pure state container — 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(); }}          >          </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(); }}          >          </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">          {() =>            state.grid.map(({ date, inMonth }) => (              <button                key={date.toISOString()}                type="button"                disabled={state.isDisabled(date)}                data-selected={state.isSelected(date) ? "" : undefined}                data-today={state.isToday(date) ? "" : undefined}                data-outside-month={!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-today:border data-today:border-input",                  "disabled:pointer-events-none disabled:opacity-30",                )}                onClick={() => { state.select(date); }}              >                {date.getDate()}              </button>            ))          }        </div>      </div>    );  }}

Install the following dependencies:

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

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 { 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 startOfMonth(year: number, month: number): Date {  return new Date(year, month, 1);}/** Builds a 6-row (42-cell) month grid, including the trailing/leading 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 CalendarStateProps {  defaultMonth?: Date;  selected?: Date;  onSelect?: (date: Date) => void;  disabled?: (date: Date) => boolean;}/** * Purely presentational + date math — no Morphos equivalent (Radix has no calendar primitive * either). `CalendarState` owns the visible month and selection; `Calendar` renders the grid it * computes. Both instantiated directly + rendered, the same pattern as every other compound * component here. */@Component()export class CalendarState extends StatefulComponent {  @Prop() defaultMonth?: Date;  @Prop() selected?: Date;  // `@Prop()`'s getter auto-invokes any raw value that is itself a function, treating it as a  // reactive `() => T` binding (see `selected` in the Calendar stories, which relies on exactly  // that to stay controlled). `onSelect`/`disabled` are real callbacks, not value-returning  // thunks — `@FunctionProp()` returns them as-is instead of calling them.  @FunctionProp() onSelect?: CalendarStateProps["onSelect"];  @FunctionProp() disabled?: CalendarStateProps["disabled"];  @State() _viewYear = 0;  @State() _viewMonth = 0;  @State() _selected: Date | undefined = undefined;  onBeforeMount() {    const base = this.defaultMonth ?? this.selected ?? new Date();    this._viewYear = base.getFullYear();    this._viewMonth = base.getMonth();    this._selected = this.selected;  }  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;  }  isSelected(date: Date): boolean {    const selected = this.selectedDate;    return selected !== undefined && isSameDay(date, selected);  }  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;  }  /** Pure state container — 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-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(); }}>          </button>          <span class={this.$s.$monthLabel}>{() => state.monthLabel}</span>          <button type="button" aria-label="Next month" class={this.$s.$navButton} onClick={() => { state.goToNextMonth(); }}>          </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}>          {() =>            state.grid.map(({ date, inMonth }) => (              <button                key={date.toISOString()}                type="button"                disabled={state.isDisabled(date)}                data-selected={state.isSelected(date) ? "" : undefined}                data-today={state.isToday(date) ? "" : undefined}                data-outside-month={!inMonth ? "" : undefined}                class={this.$s.$day}                onClick={() => { state.select(date); }}              >                {date.getDate()}              </button>            ))          }        </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
selectedDate— (controlled)
onSelect(date: Date) => void
disabled(date: Date) => boolean

For a Popover-triggered variant, see Date Picker.

On this page