Skip to contentVibraUI
Inputs & filters

Availability calendar

The mini calendar read as a booking chart: every day carries one of three fills, with a legend that names them.

A preset of MiniCalendar, not a second calendar: the same UTC day keys in marks, the same arrow-key grid, the same month ownership rules, plus the one thing a wash of colour cannot supply on its own — a key. The three fills are named in the legend and again in each day's accessible name, so the state of a night never depends on hue alone. legendLabels renames a state for the page it is on — a hotel says "Sold out" where the component says "Full" — without changing what a screen reader hears, which stays the component's own word. Decide the fill with a rule over a number rather than by hand: none left is full, a third or fewer is limited, anything above that is available — and print the number beside the calendar, because a reader who has to choose a night wants to know how many are left.

Install

npx shadcn@latest add @vibra/availability-calendar

Needs the @vibra registry in your components.json — set it up once.

Examples

Props

PropTypeDefaultDescription
…MiniCalendarPropsMiniCalendarProps—Everything MiniCalendar takes: month, value, onValueChange, marks, today, weekStartsOn, size.
legendLabelsPartial<Record<"available" | "partial" | "full", string>>—Renames a state in the legend only — "Sold out" for full — leaving the accessible names alone.

Dependencies

Source

components/ui/availability-calendar.tsx
"use client"

import * as React from "react"

import { cn } from "@/lib/utils"
import {
  MINI_CALENDAR_FILL,
  MINI_CALENDAR_FILL_LABELS,
  MiniCalendar,
  type MiniCalendarFill,
  type MiniCalendarProps,
} from "@/components/ui/mini-calendar"

export type AvailabilityFill = MiniCalendarFill

export type AvailabilityCalendarProps = MiniCalendarProps & {
  /** What each fill is called here — "Sold out" reads better than "full" on a hotel page. */
  legendLabels?: Partial<Record<AvailabilityFill, string>>
}

const ORDER: AvailabilityFill[] = ["available", "partial", "full"]

/**
 * A MiniCalendar read as a booking chart: every day carries one of three
 * fills, and the legend below says what they mean — because a wash of colour
 * is not a key, and the three states have to be nameable.
 *
 * Everything else is MiniCalendar's: the same UTC day keys in `marks`, the
 * same arrow-key grid, the same month navigation.
 */
function AvailabilityCalendar({ className, legendLabels, ...props }: AvailabilityCalendarProps) {
  return (
    <div data-slot="availability-calendar" className={cn("flex w-fit flex-col gap-2", className)}>
      {/* Full width inside whatever the caller sized this at: the legend
          below is as wide as the grid above it, or the two read as unrelated. */}
      <MiniCalendar {...props} className="w-full" />
      <ul data-slot="availability-calendar-legend" className="flex flex-wrap items-center gap-x-3 gap-y-1">
        {ORDER.map((fill) => (
          <li key={fill} className="flex items-center gap-1.5">
            <span
              aria-hidden="true"
              data-fill={fill}
              className={cn("size-3 shrink-0 rounded-[4px]", MINI_CALENDAR_FILL[fill])}
            />
            <span className="type-eyebrow text-faint-foreground">
              {legendLabels?.[fill] ??
                MINI_CALENDAR_FILL_LABELS[fill].replace(/^./, (first) => first.toUpperCase())}
            </span>
          </li>
        ))}
      </ul>
    </div>
  )
}

export { AvailabilityCalendar }