Calendar popups, scrolling time panels, range selection with hover highlighting. Everything you'd expect from a production date/time picker. Popups stay anchored to their field while the Streamlit page scrolls, flip above the field when there is no room below but there is above, and hide once the field scrolls out of view.
| Component | Description | Streamlit equivalent |
|---|---|---|
date_picker |
Calendar popup with format control, one-tap, ISO week, datetime mode | st.date_input |
date_range_picker |
Dual-calendar popup for date ranges, hover range, datetime mode | st.date_input (range mode) |
time_picker |
Time picker with scrolling panel, AM/PM | st.time_input |
time_range_picker |
Time range picker with dual panels | -- |
Lightweight alternatives with no popup. Users navigate date segments with arrow keys and typing.
| Component | Description | Streamlit equivalent |
|---|---|---|
date_input |
Keyboard-only date input (no popup) | st.date_input |
date_range_input |
Keyboard-only date range input (no popup) | st.date_input (range mode) |
| Component | Description | Streamlit equivalent |
|---|---|---|
radio_tile |
Tile-based radio group with icons and descriptions | st.radio |
select_picker |
Searchable single-select dropdown with grouping, virtualization, disabled options | st.selectbox |
tag_picker |
Searchable multi-select as removable tags; can create new options | st.multiselect |
check_picker |
Searchable multi-select dropdown with checkboxes, selected labels, and a count badge | st.multiselect |
| Component | Description | Streamlit equivalent |
|---|---|---|
check_tree |
Standalone tree with checkboxes, searchable | -- |
check_tree_picker |
Dropdown picker with checkbox tree inside | st.multiselect (flat) |
tree_picker |
Dropdown picker with single-select tree inside | st.selectbox (flat) |
cascader |
Single-select cascading column navigation | -- |
multi_cascade_tree |
Multi-select cascading column navigation | -- |
| Component | Description | Streamlit equivalent |
|---|---|---|
carousel |
Content/image carousel with autoplay, explicit local paths & URLs | -- |
timeline |
Timeline with custom react-icons | -- |
pin_input |
PIN/verification code input with masking | st.text_input |
All components are MIT licensed (RSuite is fully open-source).
- Streamlit >= 1.51: these components are built on Streamlit Components v2; 1.51 is the first release with the
st.components.v2API they rely on. st-rsuite works on every release from 1.51 onward (on 1.51 and 1.52 a small compatibility shim appliesisolate_styleson the per-call renderer; from 1.53 it is applied at registration). On older Streamlit the components cannot register and you get amust be declared ... with asset_direrror (see Troubleshooting). - Python >= 3.11
uv add st-rsuiteor with pip:
pip install st-rsuiteimport streamlit as st
from datetime import date, time, timedelta
from st_rsuite import (
date_picker, date_range_picker, time_picker,
time_range_picker, date_input, date_range_input,
radio_tile, check_tree, check_tree_picker,
multi_cascade_tree, carousel, timeline, pin_input,
)
# ── Pickers ───────────────────────────────────────────────────────────────────
d = date_picker(label="Pick a date", value=date.today(), one_tap=True, key="my_dp")
start, end = date_range_picker(
label="Trip dates",
value=(date.today(), date.today() + timedelta(days=7)),
hover_range="week",
key="my_drp",
)
t = time_picker(
label="Pick a time", value=time(9, 30),
format="hh:mm aa", show_meridiem=True, key="my_tp",
)
t_start, t_end = time_range_picker(
label="Shift hours",
value=(time(9, 0), time(17, 0)),
key="my_trp",
)
# ── Inputs ────────────────────────────────────────────────────────────────────
d2 = date_input(label="Type a date", value=date.today(), key="my_di")
start, end = date_range_input(
label="Date range",
value=(date.today(), date.today() + timedelta(days=7)),
key="my_dri",
)
# ── Selection ─────────────────────────────────────────────────────────────────
selected = radio_tile(
options=[
# "icon" is rendered as text, so any short string works (emoji included)
{"value": "a", "label": "Option A", "description": "First option", "icon": "A"},
{"value": "b", "label": "Option B", "description": "Second option", "icon": "B"},
],
value="a",
inline=True,
key="my_tile",
)
# ── Tree components ───────────────────────────────────────────────────────────
tree_data = [
{"value": "frontend", "label": "Frontend", "children": [
{"value": "react", "label": "React"},
{"value": "vue", "label": "Vue"},
]},
{"value": "backend", "label": "Backend", "children": [
{"value": "python", "label": "Python"},
{"value": "go", "label": "Go"},
]},
]
checked = check_tree(data=tree_data, searchable=True, default_expand_all=True, key="my_ct")
picked = check_tree_picker(
data=tree_data, placeholder="Select items", key="my_ctp",
)
cascade_data = [
{"value": "us", "label": "US", "children": [
{"value": "ca", "label": "California", "children": [
{"value": "sf", "label": "San Francisco"},
]},
]},
]
cascade_selected = multi_cascade_tree(data=cascade_data, key="my_mct")
# ── Carousel ──────────────────────────────────────────────────────────────────
active = carousel(
items=[
{"content": "Slide 1", "background": "#7c3aed"},
{"content": "Slide 2", "background": "#6d28d9"},
],
autoplay=True,
key="my_carousel",
)
# ── Timeline ──────────────────────────────────────────────────────────────────
timeline(
items=[
{"content": "Order placed", "time": "10:00", "icon": "FaCreditCard", "color": "#7c3aed"},
{"content": "Shipped", "time": "14:30", "icon": "FaTruck", "color": "#0891b2"},
{"content": "Delivered", "time": "11:30", "icon": "FaCheck", "color": "#059669"},
],
align="left",
key="my_timeline",
)
# ── PinInput ──────────────────────────────────────────────────────────────────
code = pin_input(length=6, mask=False, otp=True, key="my_pin")date_picker(
label="",
value=None, # date object or YYYY-MM-DD string
format="yyyy-MM-dd",
appearance="default", # 'default' | 'subtle'
size="md",
placeholder="",
placement="bottomStart",
one_tap=False, # single-click select (no OK button)
disabled=False,
cleanable=True,
block=False, # full width
iso_week=False, # Monday-start weeks
show_week_numbers=False,
editable=True, # allow keyboard entry (False = toggle-only)
loading=False, # show a loading indicator
min_date=None, # earliest selectable date (inclusive)
max_date=None, # latest selectable date (inclusive)
disabled_dates=None, # list of individual dates to disable
disabled_weekdays=None, # list of ints, 0=Monday .. 6=Sunday
limit_start_year=None, # lower year bound in the calendar
limit_end_year=None, # upper year bound in the calendar
calendar_default_date=None, # month the calendar opens on
help=None, # tooltip next to the label
locale=None, # e.g. 'ja_JP', 'zh_CN', 'es_ES'
on_change=None,
key=None,
show_meridiem=False, # 12-hour clock with AM/PM in the time panel
) -> date | datetime | NoneDatetime mode: when format carries a time field, RSuite adds a time panel and
the picker returns a datetime instead of a date. A format counts as a time
format when it contains any of H h m s anywhere, quoted literals included
(RSuite's own rule; M is month and d is day, so those do not count).
value accepts a
datetime, a date (time defaults to 00:00:00), or an ISO string of either
shape.
from datetime import datetime
when = date_picker(
label="Starts at",
value=datetime(2026, 6, 1, 9, 30),
format="yyyy-MM-dd HH:mm",
key="dp_datetime",
) # -> datetime.datetime(2026, 6, 1, 9, 30)date_range_picker(
label="",
value=None, # tuple of (date, date)
format="yyyy-MM-dd",
character=" ~ ",
appearance="default",
size="md",
placeholder="",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
iso_week=False,
show_week_numbers=False,
show_one_calendar=False, # single calendar panel
one_tap=False,
hover_range=None, # 'week' | 'month' | None
editable=True, # allow keyboard entry (False = toggle-only)
loading=False, # show a loading indicator
min_date=None, # earliest selectable date (inclusive)
max_date=None, # latest selectable date (inclusive)
disabled_dates=None, # list of individual dates to disable
disabled_weekdays=None, # list of ints, 0=Monday .. 6=Sunday
limit_start_year=None,
limit_end_year=None,
ranges=None, # shortcut presets; [] removes RSuite defaults
default_calendar_value=None, # (start, end) month the panels open on
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
show_meridiem=False, # 12-hour clock with AM/PM in the time panel
) -> tuple[date | None, date | None] | tuple[datetime | None, datetime | None]Datetime mode: the same rule applies here, and a time format turns this into a
full date-time range picker returning two datetime values. Pair a 12-hour
format such as "yyyy-MM-dd hh:mm aa" with show_meridiem=True for an AM/PM
clock.
from datetime import datetime
start, end = date_range_picker(
label="Shift window",
value=(datetime(2026, 6, 1, 8, 0), datetime(2026, 6, 1, 17, 0)),
format="yyyy-MM-dd HH:mm",
key="drp_datetime",
) # -> (datetime(2026, 6, 1, 8, 0), datetime(2026, 6, 1, 17, 0))time_picker(
label="",
value=None, # time object or HH:MM string
format="HH:mm", # 'HH:mm', 'HH:mm:ss', 'hh:mm aa'
appearance="default",
size="md",
placeholder="",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
show_meridiem=False, # AM/PM toggle
editable=True, # allow keyboard entry (False = toggle-only)
loading=False, # show a loading indicator
min_hour=None, # earliest selectable hour, 0-23 (hides earlier)
max_hour=None, # latest selectable hour, 0-23 (hides later)
hidden_hours=None, # list of hours 0-23 to hide
hidden_minutes=None, # list of minutes 0-59 to hide
hidden_seconds=None, # list of seconds 0-59 to hide
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> time | Nonetime_range_picker(
label="",
value=None, # tuple of (time, time)
format="HH:mm",
character=" ~ ",
appearance="default",
size="md",
placeholder="",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
show_meridiem=False,
editable=True,
loading=False,
min_hour=None, # earliest selectable hour, 0-23 (hides earlier)
max_hour=None, # latest selectable hour, 0-23 (hides later)
hidden_hours=None,
hidden_minutes=None,
hidden_seconds=None,
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> tuple[time | None, time | None]date_picker and date_range_picker accept declarative constraints (the
keyboard-only date_input / date_range_input do not, as RSuite has no calendar
to disable). Callables can't cross the Python/JS boundary, so pass plain values
and the frontend builds the shouldDisableDate predicate for you:
from datetime import date
from st_rsuite import date_picker
# Only weekdays in a fixed window, with a couple of holidays blocked out.
picked = date_picker(
label="Appointment",
min_date=date(2026, 6, 1),
max_date=date(2026, 6, 30),
disabled_dates=[date(2026, 6, 19)], # individual dates
disabled_weekdays=[5, 6], # 0=Monday .. 6=Sunday, so Sat/Sun
key="appt",
)min_date / max_date are inclusive. disabled_weekdays uses the same
numbering as Python's date.weekday() (Monday is 0). limit_start_year /
limit_end_year bound the years reachable in the calendar, relative to the
current selection.
date_range_picker shows shortcut presets beside the calendar. It ships RSuite's
defaults (Today, Yesterday, Last 7 days); pass your own with ranges, or
ranges=[] to hide the sidebar entirely.
from datetime import date, timedelta
from st_rsuite import date_range_picker
today = date.today()
start, end = date_range_picker(
label="Report window",
ranges=[
{"label": "Last 7 days", "value": (today - timedelta(days=6), today)},
{"label": "Last 30 days", "value": (today - timedelta(days=29), today)},
{"label": "This month", "value": (today.replace(day=1), today)},
],
key="report",
)Each preset is {"label": str, "value": (start, end)}, with optional
"close_overlay": True (commit and close on click) and
"placement": "bottom" | "left".
Simple keyboard-only components with no popups, designed for compact quick-entry scenarios.
date_input(
label="",
value=None, # date object or YYYY-MM-DD string
format="yyyy-MM-dd",
size="md",
placeholder=None,
disabled=False,
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> date | Nonedate_range_input(
label="",
value=None, # tuple of (date, date) or (str, str)
format="yyyy-MM-dd",
character=" ~ ", # separator between start and end
size="md",
placeholder=None,
disabled=False,
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> tuple[date | None, date | None]radio_tile(
options=[...], # list of dicts: {value, label, description?, icon?}
value=None, # default selected value
inline=False, # horizontal layout
disabled=False,
locale=None,
on_change=None,
key=None,
) -> str | Noneselect_picker(
items=[...], # list of dicts: {value, label, group?}
value=None, # default selected value
label="", # label above the control
searchable=True, # show search input in the dropdown
virtualized=False, # virtualize very large lists
disabled_items=None, # values rendered non-selectable
appearance="default", # 'default' | 'subtle'
size="md", # 'lg' | 'md' | 'sm' | 'xs'
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
loading=False,
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> str | NoneOptions group automatically under headings when any item carries a group key.
tag_picker(
items=[...], # list of dicts: {value, label, group?}
value=None, # list of selected values
label="",
searchable=True,
virtualized=False,
creatable=False, # let users create options not in items
disabled_items=None,
size="md",
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
loading=False,
help=None,
locale=None,
on_change=None,
key=None,
appearance="default", # 'default' | 'subtle'
) -> list[str]Multi-select rendered as removable tags. With creatable=True, values the
user types that are not in items become selectable options and come back in
the return list.
check_picker(
items=[...], # list of dicts: {value, label, group?}
value=None, # list of selected values
label="",
searchable=True,
virtualized=False,
countable=True, # append a selection-count badge
disabled_items=None,
appearance="default", # 'default' | 'subtle'
size="md",
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
loading=False,
help=None,
locale=None,
on_change=None,
key=None,
) -> list[str]Multi-select with a checkbox per option. Unlike tag_picker, the closed
control shows selected labels plus a count badge instead of removable tags.
The labels truncate as needed while the badge remains visible.
check_tree(
data=[...], # [{value, label, children?: [...]}]
value=None, # list of selected values
cascade=True, # parent/child cascade selection
searchable=True, # show search input
default_expand_all=False,
show_indent_line=False,
height=360, # tree height in px
disabled=False,
uncheckable_values=None,
locale=None,
on_change=None,
key=None,
) -> list[str]check_tree_picker(
data=[...], # [{value, label, children?: [...]}]
value=None, # list of selected values
cascade=True,
searchable=True,
countable=True, # show selected count in toggle
appearance="default",
size="md",
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
default_expand_all=False,
show_indent_line=False,
height=320,
uncheckable_values=None,
locale=None,
on_change=None,
key=None,
) -> list[str]tree_picker(
data=[...], # [{value, label, children?: [...]}]
value=None, # single selected value
label="", # label above the control
searchable=True,
virtualized=False, # virtualize very large trees
default_expand_all=False,
show_indent_line=False,
only_leaf_selectable=False, # keep branch nodes unselectable
disabled_items=None, # values rendered non-selectable
height=320, # tree height in px inside the dropdown
appearance="default",
size="md",
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
loading=False,
help=None, # tooltip next to the label
locale=None,
on_change=None,
key=None,
) -> str | NoneSingle-select counterpart of check_tree_picker.
cascader(
data=[...], # [{value, label, children?: [...]}]
value=None, # single selected value
label="",
parent_selectable=False, # allow non-leaf nodes as the answer
searchable=True,
disabled_items=None,
column_width=156, # width of each cascade column
column_height=320, # height of each cascade column
appearance="default",
size="md",
placeholder="Select",
placement="bottomStart",
disabled=False,
cleanable=True,
block=False,
loading=False,
help=None,
locale=None,
on_change=None,
key=None,
) -> str | NoneSingle-select counterpart of multi_cascade_tree. By default only leaf
nodes are selectable; set parent_selectable=True to accept any level.
multi_cascade_tree(
data=[...], # [{value, label, children?: [...]}]
value=None, # list of selected values
cascade=True,
searchable=False,
column_width=156, # width of each cascade column
column_height=320, # height of each cascade column
disabled=False,
uncheckable_values=None,
locale=None,
on_change=None,
key=None,
) -> list[str]carousel(
items=[...], # src: URL/data string or explicit pathlib.Path
autoplay=True,
autoplay_interval=4000, # ms between slides
placement="bottom", # indicator: 'top' | 'bottom' | 'left' | 'right'
shape="dot", # indicator: 'dot' | 'bar'
active_index=0,
locale=None,
on_change=None,
key=None,
) -> int # active slide indexA string
srcis always a browser reference and is never probed on the server filesystem. To inline an app-owned local image, pass apathlib.Path(or anotheros.PathLike) chosen by the app. Local assets are limited to 10 MiB each and 20 MiB per carousel render; never construct the path object from unsanitized user input.
timeline(
items=[...], # [{content, time?, icon?, color?}]
align="left", # 'left' | 'right' | 'alternate'
endless=False, # continuous timeline line
locale=None,
key=None,
) -> None # display-onlyThe icon field accepts react-icons names (e.g. "FaCheck", "FaTruck", "MdEmail") or emoji strings as fallback. 150+ icons from Font Awesome 5 and Material Design are included.
pin_input(
length=6,
value="",
mask=False, # password-style masking
type="number", # 'number' | 'alphabetic' | 'alphanumeric'
size="md",
placeholder="",
disabled=False,
read_only=False,
otp=False, # one-time password autocomplete
attached=False, # remove spacing between fields
locale=None,
on_change=None,
key=None,
) -> str # current PIN valueAll components accept a locale parameter to switch calendar labels, month/day names, and button text to the target language. RSuite ships 29 locales out of the box.
When locale is not set, the component automatically detects the browser's language (navigator.language) and uses the closest matching RSuite locale.
from st_rsuite import date_picker
# Japanese
date_picker(label="日付を選択", locale="ja_JP", one_tap=True, key="jp")
# Chinese (Simplified)
date_picker(label="选择日期", locale="zh_CN", one_tap=True, key="cn")
# Spanish
date_picker(label="Elegir fecha", locale="es_ES", one_tap=True, key="es")Available locales: ar_EG, ca_ES, cs_CZ, da_DK, de_DE, en_GB, en_US, es_AR, es_ES, fa_IR, fi_FI, fr_FR, gu_IN, hu_HU, it_IT, ja_JP, kk_KZ, ko_KR, ne_NP, nl_NL, pl_PL, pt_BR, ru_RU, sv_SE, th_TH, tr_TR, uk_UA, zh_CN, zh_TW
Locales load lazily: a page downloads only the locale it actually renders (a few KB), never the whole set.
uv add st-rsuite
uv run streamlit run examples/showcase.py# Clone and install
git clone https://github.com/lperezmo/st-rsuite.git
cd st-rsuite
uv sync --dev
# Build frontend (needs bun 1.4.4+; lint needs bun canary until it ships in a stable release)
cd st_rsuite/frontend
bun install
bun test
bun run typecheck
bun fmt --check .
bun lint
bun run build
cd ../..
# Run showcase
uv run streamlit run examples/showcase.pystreamlit.errors.StreamlitAPIException: Component 'st-rsuite.rsuite' must be declared in pyproject.toml with asset_dir to use file-backed js
Streamlit could not find st-rsuite's bundled frontend assets when it started. Check two things:
-
Streamlit version. st-rsuite needs Streamlit >= 1.51 (the first release with the
st.components.v2API). Upgrade and fully restart the app:uv add "streamlit>=1.51" # or: pip install -U "streamlit>=1.51"
-
st-rsuite install. Make sure the package is current and installed from its wheel, which ships the built assets:
uv add -U st-rsuite(orpip install -U st-rsuite), then restart the app.
Full disclaimer: This project was built with the help of Claude Opus 4.6 by Anthropic, using Claude Code and streamlit/agent-skills. It is heavily based on st-mui.
MIT