sdxc

Type to search, or start from one of these:

Calendar

A month-grid calendar surface composed from a navigable header and a native <table> of day cells, styled through the platform's own semantics.

September 2026

SunMonTueWedThuFriSat
303112345
6789101112
13141516171819
20212223242526
27282930123
45678910

Check in 2026-09-15 · weekends are unavailable, earlier dates are past

View code
let model = new CalendarModel({ focusedDate: OPENING_DAY });
let selected = dayKey(OPENING_DAY);

model.addEventListener("change", () => void handle.update());

let weeks = monthGrid(model.visibleMonth);
let days = weeks.flatMap((week) => week.days);
let monthLabel = new Intl.DateTimeFormat("en-US", { month: "long", year: "numeric" }).format(
	model.visibleMonth,
);
let focusedKey = dayKey(model.focusedDate);
let tabbable = days.find((day) => day.key === focusedKey) ?? days.find((day) => !day.outsideMonth);

<div mix={[vstack({ gap: 2, align: "start" })]}>
	<Calendar aria-label="Pick a check-in date">
		<Calendar.Header>
			<Calendar.PreviousButton
				aria-label="Previous month"
				mix={[on<HTMLButtonElement, "click">("click", () => model.showPreviousMonth())]}
			/>
			<Calendar.Heading>{monthLabel}</Calendar.Heading>
			<Calendar.NextButton
				aria-label="Next month"
				mix={[on<HTMLButtonElement, "click">("click", () => model.showNextMonth())]}
			/>
		</Calendar.Header>

		<Calendar.Grid
			aria-label={monthLabel}
			mix={[
				calendarKeys(model),
				on<HTMLTableElement, "click">("click", (event) => {
					let cell = (event.target as HTMLElement).closest("[data-date]");
					if (!(cell instanceof HTMLElement) || cell.ariaDisabled === "true") return;

					let day = days.find((candidate) => candidate.key === cell.dataset.date);
					if (day === undefined) return;

					selected = day.key;
					void handle.update();
				}),
			]}
		>
			<Calendar.GridHeader>
				<Calendar.Row>
					{WEEKDAYS.map((weekday) => (
						<Calendar.HeaderCell key={weekday}>{weekday}</Calendar.HeaderCell>
					))}
				</Calendar.Row>
			</Calendar.GridHeader>
			<Calendar.GridBody>
				{weeks.map((week) => (
					<Calendar.Row key={week.key}>
						{week.days.map((day) => (
							<Calendar.Cell
								key={day.key}
								data-date={day.key}
								tabIndex={day.key === tabbable?.key ? 0 : -1}
								aria-selected={day.key === selected ? "true" : undefined}
								aria-disabled={day.date < OPENING_DAY ? "true" : undefined}
								data-unavailable={
									day.date.getDay() === 0 || day.date.getDay() === 6 ? "" : undefined
								}
								data-outside-month={day.outsideMonth ? "" : undefined}
							>
								{day.date.getDate()}
							</Calendar.Cell>
						))}
					</Calendar.Row>
				))}
			</Calendar.GridBody>
		</Calendar.Grid>
	</Calendar>

	<p mix={[text("sm"), fg("neutral")]}>
		Check in {selected} · weekends are unavailable, earlier dates are past
	</p>
</div>

Installation

An ordinary dependency: install it, import the theme once, and import the component where it is used.

npm add @sdxc/ui
import "@sdxc/ui/theme.css";

Usage

import { Calendar } from "@sdxc/ui";

Renders a rounded, padded surface around whichever children a consumer composes; children left unset renders a native <input type="date"> through Input, the one part the platform itself operates.

Composition

The parts the component publishes, each a static property of the host.

  • Calendar
  • Calendar.HeaderRenders Calendar's nav row: a flex row spacing Calendar.PreviousButton, Calendar.Heading, and Calendar.NextButton apart, the heading centered between the controls.
  • Calendar.PreviousButtonRenders a round icon control standing for "show the previous month", sized to match Calendar.NextButton.
  • Calendar.NextButtonRenders a round icon control standing for "show the following month", with the rendering, focus, and disabled-state contract of Calendar.PreviousButton and a trailing chevron by default.
  • Calendar.HeadingRenders the consumer-formatted month/year caption through Heading, centered between the nav controls at a small semibold size layered after Heading's own defaults, so this size and alignment win over them.
  • Calendar.GridRenders the day grid's native <table> host, rows and cells set apart by a small gap on every side.
  • Calendar.GridHeaderRenders the grid's <thead> host: a plain wrapper for the weekday Calendar.Row.
  • Calendar.RowRenders a native <tr> host: one row of Calendar.HeaderCell inside Calendar.GridHeader, or one week of Calendar.Cell inside Calendar.GridBody.
  • Calendar.HeaderCellRenders a weekday abbreviation inside a native <th scope="col">, sized small and muted beneath Calendar.Cell's own larger day numbers.
  • Calendar.GridBodyRenders the grid's <tbody> host: a plain wrapper for one Calendar.Row per week.
  • Calendar.CellRenders one calendar day inside a native <td>, a fixed round pill around its number.

Examples

<Calendar aria-label={t("calendar.label")}>
	<Calendar.Header>
		<Calendar.PreviousButton aria-label={t("calendar.previous")} />
		<Calendar.Heading>{monthLabel}</Calendar.Heading>
		<Calendar.NextButton aria-label={t("calendar.next")} />
	</Calendar.Header>
	<Calendar.Grid aria-label={monthLabel}>
		<Calendar.GridHeader>
			<Calendar.Row>
				{weekdayLabels.map((day) => (
					<Calendar.HeaderCell key={day}>{day}</Calendar.HeaderCell>
				))}
			</Calendar.Row>
		</Calendar.GridHeader>
		<Calendar.GridBody>
			{weeks.map((week) => (
				<Calendar.Row key={week[0].iso}>
					{week.map((day) => (
						<Calendar.Cell
							key={day.iso}
							aria-selected={day.iso === selected ? "true" : undefined}
							data-outside-month={day.outsideMonth ? "" : undefined}
						>
							{day.label}
						</Calendar.Cell>
					))}
				</Calendar.Row>
			))}
		</Calendar.GridBody>
	</Calendar.Grid>
</Calendar>

Props

Read from the component's own types, so every prop, every default and every allowed value is listed.

PropTypeDescription
color?"brand" | "neutral" | "success" | "warning" | "danger"Semantic color role for the bare fallback control's focus ring. Defaults to DEFAULT_COLOR.
name?stringNative name submitted with an enclosing form, read only by the bare fallback control.
value?stringCurrent value, in YYYY-MM-DD form, for a bare fallback control a consumer tracks itself.
defaultValue?stringInitial value, in YYYY-MM-DD form, for a bare fallback control left to the platform's own uncontrolled state.
min?stringEarliest accepted date, in YYYY-MM-DD form, read only by the bare fallback control.
max?stringLatest accepted date, in YYYY-MM-DD form, read only by the bare fallback control.
step?numberGranularity, in days, the bare fallback control's value must fall on.
required?booleanMarks the bare fallback control required for its enclosing form.
disabled?booleanMarks the bare fallback control inert and excluded from the tab order.
readOnly?booleanMarks the bare fallback control's value fixed, while keeping it focusable and included in form submission.
autoComplete?stringNative autofill hint for the bare fallback control, e.g. "bday".

Also accepts everything in TagProps<"div">.

Calendar.Header

Also accepts everything in TagProps<"header">.

Calendar.PreviousButton

Also accepts everything in TagProps<"button">.

Calendar.NextButton

Also accepts everything in TagProps<"button">.

Calendar.Heading

Also accepts everything in Heading.Props.

Calendar.Grid

Also accepts everything in TagProps<"table">.

Calendar.GridHeader

Also accepts everything in TagProps<"thead">.

Calendar.Row

Also accepts everything in TagProps<"tr">.

Calendar.HeaderCell

Also accepts everything in TagProps<"th">.

Calendar.GridBody

Also accepts everything in TagProps<"tbody">.

Calendar.Cell

Also accepts everything in TagProps<"td">.

Types

NameValuesWhat it decides
Color"brand" | "neutral" | "success" | "warning" | "danger"Semantic color role for the bare <input type="date"> fallback's keyboard focus ring, each mapped to its matching --ui-* variables.