Calendar

ui-calendar shows one month as a grid of day buttons. Clicking a day selects it, and the selected date is the element's value in YYYY-MM-DD form. The calendar is form-associated. Give it a name and the form submits the selected date in the same format a native <input type="date"> uses.

Dates are handled as plain calendar days in UTC, so a value never shifts across time zones. Today is the user's local date. Month and weekday names come from Intl.DateTimeFormat, and weeks always start on Sunday.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <ui-calendar></ui-calendar>
</body>
</html>

Examples

Selected date

Set value to preselect a date. The calendar opens on that date's month.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <ui-calendar value="2026-03-14"></ui-calendar>
</body>
</html>

Minimum and maximum

Days before min or after max are disabled and cannot be selected.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <ui-calendar value="2026-06-10" min="2026-06-05" max="2026-06-24"></ui-calendar>
</body>
</html>

Starting month

month (YYYY-MM) sets the month shown first when there is no value to show.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <ui-calendar month="2026-12"></ui-calendar>
</body>
</html>

Locale

locale is a BCP 47 language tag for the month title and weekday names. Without it the browser's default locale is used.

Previewindex.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <ui-calendar locale="fr-FR" value="2026-07-14"></ui-calendar>
  <ui-calendar locale="ja-JP" value="2026-07-14"></ui-calendar>
</body>
</html>

Listening for changes

The calendar fires change when a day is picked. Read the new date from value.

Previewindex.htmlcalendar-change-demo.html

Selected:

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <import-element from="./calendar-change-demo.html"></import-element>
  <calendar-change-demo></calendar-change-demo>
</body>
</html>
<custom-element name="calendar-change-demo">
  <script>
    const picked = useState({ date: "nothing yet" });

    function changed(calendar) {
      picked.date = calendar.value;
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 3);
      justify-items: center;
    }
    p {
      margin: 0;
      font-size: var(--text-sm);
      color: var(--muted-foreground);
    }
  </style>

  <ui-calendar month="2026-10" onchange="changed(this)"></ui-calendar>
  <p>Selected: <slot name="#picked.date"></slot></p>
</custom-element>

In a form

The calendar submits its value under its name. Resetting the form returns it to the value attribute.

Previewindex.htmlcalendar-form-demo.html
Book Reset
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Calendar demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/calendar.html"></import-element>

  <import-element from="./calendar-form-demo.html"></import-element>
  <calendar-form-demo></calendar-form-demo>
</body>
</html>
<custom-element name="calendar-form-demo">
  <script>
    const result = useState({ text: "Submit the form to see its data." });

    function submit(event) {
      event.preventDefault();
      result.text = [...new FormData(event.target)].map(([name, value]) => `${name}=${value}`).join("\n");
    }

    function reset() {
      result.text = "The form was reset.";
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 4);
      justify-items: start;
    }
    form {
      display: grid;
      gap: calc(var(--spacing) * 3);
      justify-items: start;
    }
    .actions {
      display: flex;
      gap: calc(var(--spacing) * 2);
    }
    output {
      display: block;
      box-sizing: border-box;
      width: 100%;
      padding: calc(var(--spacing) * 3);
      border-radius: var(--radius-md);
      background: var(--muted);
      font-family: var(--font-mono);
      font-size: var(--text-xs);
      white-space: pre-wrap;
    }
  </style>

  <form onsubmit="submit(event)" onreset="reset()">
    <ui-calendar name="checkin" value="2026-08-03" min="2026-08-01"></ui-calendar>
    <div class="actions">
      <ui-button type="submit">Book</ui-button>
      <ui-button type="reset" variant="outline">Reset</ui-button>
    </div>
  </form>
  <output><slot name="#result.text"></slot></output>
</custom-element>

API

Attributes

Attribute Type Default Description
value string "" Selected date as YYYY-MM-DD. Empty means no selection.
month string Month shown first, as YYYY-MM. Used when the calendar is created. Defaults to the month of value, or the current month.
min string Earliest selectable date, YYYY-MM-DD.
max string Latest selectable date, YYYY-MM-DD.
locale string Locale for the month title and weekday names. Defaults to the browser locale.

Properties

Property Type Description
value string The selected date. Setting it updates the selection and the submitted value.

Events

Event Detail Description
change none Fired after the user picks a day.

CSS parts

Part Description
header The row with the previous and next month buttons and the month title.
grid The <table> holding the weekday headings and days.
day Each day <button>. It carries data-date, plus data-outside for days of the neighbouring months, data-today for today, aria-selected="true" for the selected day and disabled outside min/max.

::part(day) accepts pseudo-classes such as :hover and :disabled but not attribute selectors, so the selected, today and outside colours come from the theme tokens (--primary, --accent, --muted-foreground).

Forms

ui-calendar is form-associated. With a name, the form submits value (YYYY-MM-DD), or nothing when no date is selected. form.reset() restores the value attribute, and the browser's form state restore brings back a selected date after navigation.

Accessibility

The days are a table with role="grid", and the selected day has aria-selected="true". The month title is an aria-live="polite" region, so month changes are announced. Only one day is in the tab order at a time (the selected day, or today when nothing is selected).

Key Action
Arrow Left / Arrow Right Move focus one day back or forward.
Arrow Up / Arrow Down Move focus one week back or forward. Moving past the edge of the month shows the next or previous month.
Enter / Space Select the focused day.
Tab Leave the grid. The previous and next month buttons come before it in tab order.

Edit this page on GitHub · View as Markdown