reflex-schedule-x
Schedule-X event calendar for Reflex — a modern alternative to FullCalendar and react-big-calendar, driven entirely from Python.
- 6 views: day, week, month grid, month agenda, week agenda and list.
- Events in Reflex state: pass a
list[dict]with plain date strings; edits in Python update the running calendar. - Every callback (
on_event_click,on_range_update,on_click_date_time,on_view_change, ...) as typed Reflex event triggers. - Plugins: events service, calendar controls, current time, event modal, scroll controller, event recurrence (RRULE / EXDATE), iCalendar import, timezone select.
- Custom components: fill any Schedule-X slot (
timeGridEvent,monthGridEvent,eventModal, header slots, ...) with Reflex components. - i18n & timezones: 37 locales, translation overrides, IANA timezones, dark mode,
defaultandshadcnthemes. - Standalone date picker and time picker components.
- Imperative API (
ScheduleXAPI) forrx.call_scriptdriven control.
Bundles @schedule-x/* 4.8.0 (open-source packages). Premium plugins (drag & drop, resize, drag-to-create,
resource scheduler, ...) are not included.
Installation
pip install reflex-schedule-x
# or
uv add reflex-schedule-x
Requires reflex>=0.9.11. The npm packages are installed automatically by Reflex.
Quick start
import reflex as rx
from reflex_schedule_x import calendar_type, schedule_x
class State(rx.State):
events: list[dict] = [
{
"id": "1",
"title": "Coffee with John",
"start": "2026-09-17 10:05",
"end": "2026-09-17 10:35",
"calendarId": "work",
},
{"id": "2", "title": "Ski trip", "start": "2026-09-19", "end": "2026-09-21"},
]
selected: dict = {}
@rx.event
def on_event_click(self, event: dict):
self.selected = event
def index():
return schedule_x(
events=State.events,
calendars={"work": calendar_type("work", "#f91c45", "#ffd2dc", "#59000d", label="Work")},
views=["day", "week", "month-grid", "month-agenda"],
default_view="week",
timezone="America/Caracas",
locale="es-ES",
is_dark=rx.color_mode_cond(light=False, dark=True),
event_modal=True,
current_time_indicator=True,
on_event_click=State.on_event_click,
height="800px",
)
app = rx.App()
app.add_page(index)
Dates and times
Schedule-X uses the Temporal API. The component converts strings for you:
| Value you pass | Meaning |
|---|---|
"2026-09-17" |
All-day date (Temporal.PlainDate) |
"2026-09-17 10:00" or "2026-09-17T10:00" |
Wall-clock time in the calendar timezone |
"2026-09-17T10:00:00-04:00" / "...Z" |
Absolute instant, shown in the calendar timezone |
"2026-09-17T10:00:00+02:00[Europe/Berlin]" |
RFC 9557 zoned date-time |
Payloads sent back to Python use "YYYY-MM-DD" for dates and "YYYY-MM-DD HH:mm" (calendar timezone) for
date-times, so they round-trip with your state. Set datetime_format="iso" to receive RFC 9557 strings instead.
Helpers: to_sx_datetime(date | datetime), to_sx_date(...), parse_sx_datetime(str),
calendar_event(...), background_event(...), calendar_type(...).
schedule_x props
Props marked live update the running calendar; changing any other prop re-creates it.
| Prop | Type | Notes |
|---|---|---|
events |
list[dict] |
live. id, start, end, title, description, location, people, calendarId, rrule, exdate, _options, _customContent, any custom key |
background_events |
list[dict] |
live. start, end, style, title, rrule, exdate |
calendars |
dict[str, dict] |
live. colorName, label, lightColors, darkColors |
views |
list[str] |
day, week, month-grid, month-agenda, week-agenda, list |
default_view |
str |
Initial view |
view |
str |
live, controlled view (pair with on_view_change) |
selected_date |
str |
live, YYYY-MM-DD |
min_date / max_date |
str |
live |
locale |
str |
live, see reflex_schedule_x.LOCALES |
timezone |
str |
live, IANA name, default UTC |
first_day_of_week |
int |
live, 1 = Monday … 7 = Sunday |
translations |
dict |
Overrides merged over the built-in texts: {"en-US": {"Week": "4 days"}} |
day_boundaries |
dict |
live, {"start": "06:00", "end": "18:00"} |
week_options |
dict |
live, gridHeight, nDays, eventWidth, gridStep, eventOverlap, timeAxisFormatOptions |
month_grid_options |
dict |
live, {"nEventsPerDay": 4} |
month_agenda_options |
dict |
{"nEventIndicatorsPerDay": 3} |
show_week_numbers, is_responsive, skip_animations, skip_validation |
bool |
|
small_breakpoint |
int |
Width (px) under which small-screen views are used |
date_picker |
dict |
Header date picker options |
theme |
"default" | "shadcn" |
Literal value; use one theme per app |
is_dark |
bool |
live |
current_time_indicator, current_time_full_week_width |
bool |
current-time plugin |
event_modal |
bool |
event-modal plugin |
initial_scroll / scroll_to |
str |
scroll-controller plugin; scroll_to is live |
recurrence |
bool |
event-recurrence plugin |
ical_data |
str |
iCalendar plugin (times are treated as UTC) |
timezone_select |
bool |
timezone-select plugin |
datetime_format |
"naive" | "iso" |
Format of date-times in payloads |
id |
str |
Enables ScheduleXAPI(id) |
The calendar defaults to width="100%", height="800px", max_height="90vh"; override with normal style props.
Event triggers
| Trigger | Payload |
|---|---|
on_event_click, on_double_click_event, on_event_update |
event dict (plus is_all_day) |
on_range_update |
{"start": str, "end": str} |
on_calendar_render |
{"view": str, "date": str, "range": {...}} |
on_view_change |
view name |
on_selected_date_update, on_click_date, on_double_click_date, on_click_agenda_date, on_double_click_agenda_date, on_click_plus_events, on_scroll_day_into_view |
"YYYY-MM-DD" |
on_click_date_time, on_double_click_date_time |
"YYYY-MM-DD HH:mm" |
on_slot_action |
{"action", "slot", "event", "date"} |
on_error |
error message |
Lazy loading: load events in on_range_update (and on_scroll_day_into_view for the list view) and assign them to
the events state var.
Custom components (slots)
Put schedule_x_slot(name, *children) components inside schedule_x. Slot content is normal Reflex UI, so it can use
state. Inside a slot:
schedule_x_field(name, format=None)renders slot data. Event fields:title,description,location,people,start,end,calendarId, custom keys; virtual fieldstime_range,date_range,calendar_label. Other slots exposedate,day,hour,events. Formats:time,date,datetime,weekday,weekday_short,day,month,hour,count,upper,json.schedule_x_show(name, *children, negate=False)renders children when the field is truthy (is_all_daytoo).schedule_x_event_card(*children, variant="container" | "main")paints the event's calendar colors.schedule_x_action(*children, action="delete", close_modal=True)sends clicks toon_slot_action.
schedule_x(
schedule_x_slot(
"timeGridEvent",
schedule_x_event_card(
schedule_x_field("title", tag_name="strong"),
schedule_x_field("time_range"),
schedule_x_show("location", schedule_x_field("location")),
),
),
schedule_x_slot(
"eventModal",
rx.card(
schedule_x_field("title", tag_name="h3"),
schedule_x_action(rx.button("Delete"), action="delete", close_modal=True),
),
),
schedule_x_slot("headerContentRightPrepend", rx.button("New", on_click=State.new_event)),
events=State.events,
event_modal=True,
on_slot_action=State.on_slot_action,
)
Supported slots: timeGridEvent, dateGridEvent, monthGridEvent, monthAgendaEvent, weekAgendaEvent,
monthAgendaDateDots, eventModal, headerContent, headerContentLeftPrepend, headerContentLeftAppend,
headerContentRightPrepend, headerContentRightAppend, weekGridDate, weekGridHour, monthGridDayName,
monthGridDate.
Imperative API
from reflex_schedule_x import ScheduleXAPI
api = ScheduleXAPI("my-calendar") # schedule_x(id="my-calendar")
rx.button("Week", on_click=api.set_view("week"))
rx.button("Today", on_click=api.set_date(datetime.date.today()))
rx.button("Dump", on_click=api.get_events(callback=State.receive_events))
Methods: set_view, set_date, set_theme, add_event, update_event, remove_event, set_events,
close_event_modal, scroll_to, get_view, get_date, get_range, get_events, get_event.
Date and time pickers
schedule_x_date_picker(value=State.date, on_change=State.set_date, locale="es-ES", label="Fecha", min="2026-01-01")
schedule_x_time_picker(value=State.time, on_change=State.set_time, is_12_hour=True, label="Hora")
Demo app
The schedule_x_demo/ folder contains a multi-page demo: playground with every option, events CRUD and callback
log, custom components, recurrence + background events + iCal, language and timezones, imperative API with the list
view, and the pickers.
uv venv && uv pip install -e .
cd schedule_x_demo
uv pip install -r requirements.txt
uv run reflex run
Development
uv sync --extra dev
uv run ruff check . && uv run ruff format --check .
uv run pytest --cov
uv run reflex component build # generates .pyi stubs and builds sdist + wheel
Branches: work happens on develop; main only receives pull requests from develop. The Quality (ruff, pytest on
Python 3.10–3.13, build, stub freshness, demo bundle) and Security (CodeQL, Bandit, pip-audit, Gitleaks, dependency
review) workflows must pass before merging.
Releasing
-
Bump
versioninpyproject.toml, updateCHANGELOG.md, and mergedevelopintomain. -
Tag
mainand push the tag:git checkout main && git pull git tag -a v0.1.0 -m "v0.1.0" git push origin v0.1.0
The Release workflow checks that the tag matches the version, runs the tests, builds the sdist and wheel, publishes
them to PyPI with Trusted Publishing (no API token) from the pypi environment, and creates the GitHub release with
the files attached.
License
MIT © Ernesto Crespo. Schedule-X is MIT licensed, © Tom Österlund.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file reflex_schedule_x-0.1.0.tar.gz.
File metadata
- Download URL: reflex_schedule_x-0.1.0.tar.gz
- Upload date:
- Size: 36.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
99abc3a9f4b770495d02746ad8561b54a627386b36a2432bdcf51f731ed6b414
|
|
| MD5 |
2cdb21f493349256c4b4f77bfccf38e2
|
|
| BLAKE2b-256 |
ae247ebfdf070d510ce313668d6bb18d6b3e4b3d20712d9456ae8bee10de5e39
|
Provenance
The following attestation bundles were made for reflex_schedule_x-0.1.0.tar.gz:
Publisher:
release.yml on ecrespo/reflex-schedule-x
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reflex_schedule_x-0.1.0.tar.gz -
Subject digest:
99abc3a9f4b770495d02746ad8561b54a627386b36a2432bdcf51f731ed6b414 - Sigstore transparency entry: 2878597859
- Sigstore integration time:
-
Permalink:
ecrespo/reflex-schedule-x@13c01f5a95e79a2bfcbd3840a98c894208d41f80 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ecrespo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@13c01f5a95e79a2bfcbd3840a98c894208d41f80 -
Trigger Event:
push
-
Statement type:
File details
Details for the file reflex_schedule_x-0.1.0-py3-none-any.whl.
File metadata
- Download URL: reflex_schedule_x-0.1.0-py3-none-any.whl
- Upload date:
- Size: 32.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cab4fb232818a38947c410f1a5cc72efee90e7707dc84e32a2bc98bd70b75a79
|
|
| MD5 |
196ef937646941e938ba9c446c2c641a
|
|
| BLAKE2b-256 |
9574e296eb799dc87b6f320a2f4688edeb4837e97ef23cd9e510b19f12280ead
|
Provenance
The following attestation bundles were made for reflex_schedule_x-0.1.0-py3-none-any.whl:
Publisher:
release.yml on ecrespo/reflex-schedule-x
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
reflex_schedule_x-0.1.0-py3-none-any.whl -
Subject digest:
cab4fb232818a38947c410f1a5cc72efee90e7707dc84e32a2bc98bd70b75a79 - Sigstore transparency entry: 2878597907
- Sigstore integration time:
-
Permalink:
ecrespo/reflex-schedule-x@13c01f5a95e79a2bfcbd3840a98c894208d41f80 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/ecrespo
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@13c01f5a95e79a2bfcbd3840a98c894208d41f80 -
Trigger Event:
push
-
Statement type: