How to build a month calendar in React Native

By the LumioUI team · Updated

A month calendar is a 7-column grid of days with blanks before the 1st, a header to page between months, and a mark on days that have something on. You don't need a library to build one in React Native: work out the grid from the first weekday and the number of days in the month, render it with flexWrap, and key every day by its ISO date. The LumioUI Calendar adds busy-day dots, swiping between months, folding into a single week and an agenda that swaps below.

Recording of the LumioUI Calendar component (Pro).

How a month calendar works

  1. The grid. new Date(y, m, 1).getDay() gives the number of blanks before the 1st; new Date(y, m + 1, 0).getDate() gives the number of days. Pad the end to a multiple of 7.
  2. Key days by ISO string. "2026-10-14" compares and looks up cleanly. Build it from local date parts, not toISOString(), which converts to UTC and can shift the day.
  3. Busy days. Turn your events into a map of date to count, and draw up to three dots under each day.
  4. Paging. Keep the visible month in state; next and previous set it to the 1st of the neighbouring month. Swiping can drive the same state.
  5. Accessibility. Each day is a button with a full spoken date, its event count and selected state. The month title is a header.
TSX MonthCalendar.tsx
import * as React from "react";
import { Pressable, StyleSheet, Text, View } from "react-native";

const WEEKDAYS = ["S", "M", "T", "W", "T", "F", "S"];
const iso = (d: Date) =>
  `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;

/** The month as 7-day rows: leading and trailing blanks are null. */
function monthGrid(month: Date): (Date | null)[] {
  const y = month.getFullYear(), m = month.getMonth();
  const cells: (Date | null)[] = Array(new Date(y, m, 1).getDay()).fill(null); // 0 = Sunday
  const days = new Date(y, m + 1, 0).getDate();
  for (let d = 1; d <= days; d++) cells.push(new Date(y, m, d));
  while (cells.length % 7) cells.push(null);
  return cells;
}

export function MonthCalendar({ events, selected, onSelect }: {
  events: Record<string, number>; // "yyyy-MM-dd" -> event count
  selected: string;
  onSelect: (day: string) => void;
}) {
  const [month, setMonth] = React.useState(() => new Date());
  const shift = (n: number) => setMonth((d) => new Date(d.getFullYear(), d.getMonth() + n, 1));
  const today = iso(new Date());

  return (
    <View>
      <View style={styles.header}>
        <Pressable onPress={() => shift(-1)} accessibilityLabel="Previous month" hitSlop={12}><Text>‹</Text></Pressable>
        <Text accessibilityRole="header" style={styles.title}>
          {month.toLocaleDateString(undefined, { month: "long", year: "numeric" })}
        </Text>
        <Pressable onPress={() => shift(1)} accessibilityLabel="Next month" hitSlop={12}><Text>›</Text></Pressable>
      </View>
      <View style={styles.grid}>
        {WEEKDAYS.map((w, i) => <Text key={i} style={styles.weekday}>{w}</Text>)}
        {monthGrid(month).map((day, i) => {
          if (!day) return <View key={i} style={styles.cell} />;
          const key = iso(day);
          const count = events[key] ?? 0;
          const on = key === selected;
          return (
            <Pressable key={key} style={styles.cell} onPress={() => onSelect(key)}
              accessibilityRole="button" accessibilityState={{ selected: on }}
              accessibilityLabel={`${day.toDateString()}${count ? `, ${count} events` : ""}`}>
              <View style={[styles.day, on && styles.dayOn]}>
                <Text style={[key === today && styles.today, on && styles.textOn]}>{day.getDate()}</Text>
              </View>
              <View style={styles.dots}>
                {Array.from({ length: Math.min(count, 3) }, (_, d) => <View key={d} style={styles.dot} />)}
              </View>
            </Pressable>
          );
        })}
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  header: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", paddingVertical: 12 },
  title: { fontSize: 17, fontWeight: "600" },
  grid: { flexDirection: "row", flexWrap: "wrap" },
  weekday: { width: `${100 / 7}%`, textAlign: "center", opacity: 0.5, paddingBottom: 6 },
  cell: { width: `${100 / 7}%`, alignItems: "center", paddingVertical: 4 },
  day: { width: 36, height: 36, borderRadius: 18, alignItems: "center", justifyContent: "center" },
  dayOn: { backgroundColor: "#111" },
  textOn: { color: "#fff" },
  today: { fontWeight: "700" },
  dots: { flexDirection: "row", gap: 2, height: 6, marginTop: 2 },
  dot: { width: 4, height: 4, borderRadius: 2, backgroundColor: "#888" },
});

Need the week to start on Monday? Use (getDay() + 6) % 7 for the blanks and reorder the weekday labels.

The quick way: LumioUI Calendar

The Calendar shows up to three coloured dots per day, swipes between months, folds into the selected week, jumps back with a Today pill and swaps the agenda below as you pick a day. It's part of LumioUI Pro.

TSX Schedule.tsx
import * as React from "react";
import { Calendar } from "@/components/ui/calendar/calendar";

const EVENTS = [
  { date: "2026-10-14", category: "work", title: "Design review" },
  { date: "2026-10-14", category: "personal", title: "Climbing" },
  { date: "2026-10-21", category: "work", title: "Launch" },
];

export function Schedule() {
  const [day, setDay] = React.useState<Date>(new Date());
  return (
    <Calendar
      selected={day}
      onSelect={setDay}
      events={EVENTS}
      categories={{ work: "teal", personal: "orange" }}
      renderAgenda={(date, events) => <DayAgenda date={date} events={events} />}
    />
  );
}
  • selected and onSelect make it controlled (ISO string or Date); defaultSelected lets it manage itself.
  • events with categories colour the dots; markedDates is a shorthand for a single dot.
  • availableDates turns the grid into a map of open days for booking; disabledDates, minDate and maxDate limit what can be picked.
  • collapsed folds it to one week. For a week strip see Calendar Strip, and for check-in and check-out, Date Range Picker.

Add it to your app

LumioUI's CLI copies the component's source into your project, so it's yours to read and change. Run init once per project:

npx @lumioui/cli init
npx @lumioui/cli add pro/calendar

It works in Expo (SDK 54+) and bare React Native, with Reanimated 4 and NativeWind. See the installation guide and the full Calendar API.Calendar is part of LumioUI Pro: Pro installs need your licence key (CLI guide, pricing).

More React Native guides