Streamlit Canary User Manual
Streamlit Canary is a fork of Streamlit with an event-driven runtime and a new component library called Components V3. It keeps Streamlit’s familiar look and feel, but removes the rerun cycle that makes large apps slow.
The application function runs once. It builds a persistent component tree, and every later interaction is routed to a signal handler on the server. Only the values that actually changed are sent back to the browser as small patches, and the page updates in place.
What makes it different
| Streamlit | Streamlit Canary (V3) | |
|---|---|---|
| Execution model | The script reruns on every interaction | The app function runs once |
| UI updates | A new render tree is diffed on every rerun | A delta patch updates the affected DOM nodes |
| State | st.session_state is rebuilt on each rerun | State holds Property objects for the session |
| Widgets | Return values from the last run | Expose reactive Property fields and Signals |
| Cost of one click | Proportional to the whole script | Proportional to the handlers that listen to it |
Highlights
- Performant by design. The app function runs once. An interaction executes only the handlers attached to it, then patches the DOM. The script never re-runs.
- Reactive, polished event-driven syntax.
sc.bindandsc.bbindbuild derived and two-way values,on_changeand component signals attach handlers, and a forced notification (set(value, notify=True)) refreshes output when the value itself did not change. - Desktop windows.
sc.run(..., show_window=True)serves the app and opens it in a nativepyapp-windowwindow, sized in CSS pixels. - Custom components included. Folder trees,
PathInputBrowse, a PDF viewer, a live log panel, animated toasts, and the rest of the V3 library ship with the package. dunder_literal.PageConfig(dunder_literal=True)keeps names such as__init__and__name__from turning into bold text.- Fewer dependencies. The core does not require pandas, NumPy, SciPy,
pyarrow, or Altair. Altair is needed only if you use
AltairChart.
Installation
pip install streamlit-canary
Python 3.11 or newer is required.
Note
Streamlit Canary 0.5.0 is the event-driven API, and it is the default: the components are exported at the top level (
sc.Button,sc.Text,sc.Row, …).Two earlier generations are no longer part of
sc:
- the V1 wrappers (Streamlit component wrappers, rerun model) now live in
streamlit_canary.legacy_streamlit_wrappers;- the V2 experiment has been removed.
sc.v3.*still resolves, but it is deprecated and warns – the prefix goes away in a later release.
Hello world
import streamlit_canary as sc
class State(sc.State):
count = sc.Property(0)
state = State()
def app():
sc.PageTitle('Click Counter')
with sc.Row('center'):
with sc.Text('Click count: 0') as txt:
@state.count.on_change
def _():
txt.text.set('Click count: {}'.format(state.count.get()))
with sc.Button('Increase counter', type='primary') as btn:
@btn.on_click
def _():
state.count.set(state.count.get() + 1)
# or a shorthand form: state['count'] += 1
if __name__ == '__main__':
sc.run(app, port=2201)
Save the file and run it with python app.py. Open
http://localhost:2201, then click the button. The text changes without a
page reload, and app() is not called again.
How this manual is organized
- Components explains the runtime model, session state, and every component in the library.
- Legacy Streamlit Wrappers documents the original Streamlit-based API,
which now lives in
streamlit_canary.legacy_streamlit_wrappers. - Roadmap lists the work that is planned next.
If you are starting a new app, read Running Apps, The Event Driven System, and Session State first, then use the API Reference as a lookup.
Running Apps
sc.run starts a Streamlit Canary app. It accepts either the app function
itself or a path to a script or module.
import streamlit_canary as sc
def app():
sc.PageTitle('Hello')
sc.Text('This page has no rerun loop.')
if __name__ == '__main__':
sc.run(app, port=2201)
sc.run
sc.run(
target,
port=3001,
*,
blocking=True,
host=None,
show_window=False,
window_size=(1200, 900),
window_pos='center',
window_title=None,
window_icon=None,
icon=None,
title=None,
extra_args=(),
)
| Argument | Type | Meaning |
|---|---|---|
target | callable or str | The app function, a .py path, or a module path |
port | int | Port to listen on, default 3001 |
blocking | bool | Whether run() waits for the app to end |
host | str or None | Interface to bind |
show_window | bool | Open the app in a native window |
window_size | tuple or keyword | Window size in CSS pixels |
window_pos | position | Window position, default 'center' |
window_title | str | Window title |
window_icon | str | Window icon path |
icon / title | str | Aliases of window_icon / window_title |
extra_args | sequence of str | Extra arguments for a path target |
It returns a pair:
server_process, window_process = sc.run(app)
Either element may be None when that process does not exist.
Callable targets
A callable is served in the current process:
sc.run(app, port=2201)
With blocking=True (the default), run() does not return until the
server stops. With blocking=False, the server is started on a daemon
thread and run() returns at once; the caller must keep the process
alive.
Path targets
A .py path is launched in a child process:
sc.run('app.py', port=2201)
A module path is launched with python -m:
sc.run('my_project.app', port=2201)
Arguments left over from the command line can be forwarded:
sc.run('app.py', extra_args=('--debug',))
The child process calls sc.run again on its own. Return values therefore
describe the child process.
Host
host chooses the interface to bind:
'0.0.0.0': all interfaces; the app is reachable on the LAN.'127.0.0.1': this machine only.'localhost': this machine only, including IPv6 loopback.
When host is omitted, it follows show_window:
show_window=True:'localhost', because a desktop window is a local session and binding every interface would only earn a firewall prompt;- otherwise:
'0.0.0.0'.
Opening a native window
show_window=True serves the app and opens it in a desktop window through
pyapp-window:
sc.run(
app,
port=2201,
show_window=True,
window_size=(980, 760),
window_title='My App',
window_icon='assets/app.ico',
)
The window is not a browser tab; it is a native webview. Closing it ends
the session. With blocking=True, the server is stopped as well.
window_size
window_size is the page viewport, in CSS pixels. A 700x600 window
shows 700x600 CSS pixels of the page regardless of the display’s scale
factor, so the layout sees exactly the size requested.
window_size=(1200, 900) # width, height
window_size='maximized'
window_size='fullscreen'
The window manager may crop the requested box when it does not fit the screen. Size the window from the layout the app builds, not from a fixed guess: a form with a known number of rows has a known height.
window_pos
The default is 'center'. A screen position can also be passed the way
pyapp-window accepts it. Leave it alone unless the app has a reason to
place itself.
window_title and window_icon
window_title sets the OS window title. If omitted, the title is
'Streamlit Canary Application'.
window_icon points to an icon file for the window.
title and icon are shorter aliases:
sc.run(app, show_window=True, title='My App', icon='app.ico')
Platform note
The window backend is provided by pyapp-window. On Windows, its toga
backend requires .NET 10. If the runtime is installed outside the default
location, point DOTNET_ROOT at it before starting the app.
Blocking behavior
| Target | blocking=True | blocking=False |
|---|---|---|
| Callable | Serves in the current thread until stopped | Serves on a daemon thread and returns |
| Path / module | Waits for the child process | Leaves the child running and returns |
With show_window=True and blocking=True, run() waits for the window
to close and then stops the server.
Stopping an app
- Press Ctrl+C in the terminal that started a blocking server.
- With a native window, close the window.
- With
blocking=False, the caller owns the process (for a callable) or the returned child process (for a path target).
The server exposes a /shutdown endpoint, which is how a window session
asks the serving child to stop cleanly before falling back to terminating
it.
Related pages
Components
The components are the event-driven library. It has no dependency on
Streamlit: every component is a plain Python object that owns reactive
Property fields and emits Signals.
Two rules to remember
- Build the tree once. The app function passed to
sc.runis called once. Components created inside it stay alive for the session. - Change values, not the tree. Attach handlers to signals, then update properties. The runtime sends the difference to the browser.
A first component
import streamlit_canary as sc
with sc.Button('Say hello') as button:
@button.on_click
def _():
print('clicked')
The with block is how a component is added to its parent. Outside a
with block it is attached to the current component on the context stack,
which is the app root by default.
Namespace
Since 0.5.0 the components are exported at the top level – sc.Button,
sc.Text, sc.Row:
import streamlit_canary as sc
sc.Button
sc.Text
sc.Row
sc.v3.* still resolves for apps written before 0.5.0, but that form is
deprecated: it emits a DeprecationWarning.
The kernel is available at the top level:
sc.Property
sc.Signal
sc.State
sc.bind
sc.bbind
sc.run
Component groups
The library follows Streamlit’s API reference categories:
| Module | Components |
|---|---|
texts | Text, Markdown, Title, PageTitle, Caption, Code |
data | Table |
charts | AltairChart |
buttons | Button, IconButton, MenuButton, ToggleButton |
inputs | Checkbox, CheckGroup, Radio, RadioGroup, Selectbox, Multiselect, NumberInput, TextInput, TextArea, Toggle, SegmentedControl, SelectSlider, ReducibleGroup |
layouts | Row, Column, Container, Expander, Grid, Tabs, Dialog, Popover, FloatingContainer, Space, BottomContainer |
status | Info, Success, Warning, Error, Callout, Spinner, Progress, Toast, LogPanel |
media | PdfViewer |
page | PageConfig |
trees | SingleTreeSelect, TreeView, FileTreeView, DualTreeSelect, ColumnView, PathInput, PathSelect, PathInputBrowse |
Shared conventions
Sizing
Every component accepts the same size keywords:
sc.Button('Go', width='stretch')
sc.Container(height=320)
sc.Text('x', min_height=40)
width/height: anintin CSS pixels,'stretch','content', or'auto'(where the component supports it).max_width/min_width/max_height/min_height: positive integers orNone.
A capped height makes the box scroll. 'stretch' on one axis drops that
axis’s min / max bounds.
Visibility
Most components take visible. Text-like components default to
'auto': they show when their content is non-empty and hide when it is
blank. A boolean or a bound property is used as given.
sc.Text('') # hidden
sc.Text('hello') # shown
sc.Text('hello', visible=False) # hidden
sc.Text('', visible=True) # shown, but empty
To reserve a line without text, use exactly one space: sc.Title(' ').
Reading and writing
All reactive fields use the same style:
widget.value.get()
widget.value.set(42)
widget['value'] # same as .get()
widget['value'] = 42 # same as .set(42)
widget['on_value'] # same as widget.value.on_change
Binding
sc.bind(source, transform) creates a read-only, derived property.
sc.bbind(source) creates a two-way binding for widgets that write back.
sc.Text(sc.bind(state.count, lambda n: 'Count: {}'.format(n)))
sc.NumberInput('Channel', sc.bbind(state.channel))
Pages in this section
The Event Driven System
Streamlit Canary replaces Streamlit’s rerun loop with a persistent component tree and a small event protocol.
The old model
In Streamlit, a widget interaction triggers a full script rerun:
click -> rerun script -> rebuild the UI -> diff -> send the page
The cost grows with the size of the script, not with the size of the change. A large app pays the whole price for one checkbox.
The V3 model
In V3, the app function runs once:
start -> run app() once -> build the component tree -> render HTML
click -> WebSocket event -> find the handler -> update properties
-> send delta patches -> patch the DOM
The app function is never called again for that session. A handler runs only the code registered on the signal that fired.
The pieces
Runtime
sc.run(app_function, port=2201) starts a Starlette + Uvicorn server. The
runtime keeps one component tree per session and owns the WebSocket
endpoint. It is a no-rerun runtime: there is no script re-execution path.
See Running Apps for sc.run’s full argument list
and for opening the app in a native window.
Component tree
Components are nested with context managers:
with sc.Row('center'):
sc.Text('Left')
sc.Button('Right')
Each component has an id, a parent, and children. The tree is built
once and kept for the life of the session.
Properties
A Property is a reactive value container:
count = sc.Property(0)
count.set(1)
print(count.get()) # 1
Setting a value emits on_change only when the value actually changes.
Pass notify=True to force a notification when something outside the
value changed but the rendered result still needs a refresh.
Signals
A Signal is a publish / subscribe primitive with no value:
clicked = sc.Signal()
@clicked
def handler():
print('clicked')
clicked.emit()
Signals may declare payload types:
changed = sc.Signal(int)
changed.emit(42)
Handlers
A component exposes named signals:
with sc.Button('Save') as button:
@button.on_click
def _():
...
on_click, on_change, on_submit, and the other signals are ordinary
Signal objects. A handler runs on a daemon worker thread, so a long
operation does not block the WebSocket loop.
Delta patches
When a property changes, the runtime serializes the new value and sends a patch naming the component and the field. The browser updates that field in place. No HTML page is reloaded.
Some elements own client-side state that never reaches the server:
expanding an Expander, switching a Tabs panel, opening a Popover.
Those interactions are handled entirely in the browser.
Binding values
sc.bind derives a property from one or more sources:
label = sc.bind(state.count, lambda n: 'Count: {}'.format(n))
The derived property updates when a source changes. A Signal may be
listed beside the value sources as a trigger: it forces a recomputation
even when the value comes out the same, which is useful when the rendered
result depends on more than the value.
label = sc.bind(
(state.items, state.reloaded),
lambda pair: ', '.join(pair[0]),
)
sc.bbind binds in both directions:
sc.NumberInput('Channel', sc.bbind(state.channel))
The widget mirrors the state, and a user edit writes back into it.
Special markers
sc._self, sc._value, and sc._undefined are sentinel objects used by
the kernel. They are not values; they tell a handler or a binding how to
resolve something at the moment it runs.
sc._self
Bind sc._self in .partial(...) to receive the signal’s owner:
@state.count.on_change.partial(sc._self)
def _(prop):
print(prop.get())
For a property, the owner is the Property itself. For a component
signal such as button.on_click, the owner is the component:
@button.on_click.partial(sc._self)
def _(btn):
print(btn.text.get())
Without .partial(...), a handler receives whatever the signal emits. Use
sc._self when the handler should read several fields of the owner, or
when it needs the handle rather than a snapshot of its value.
sc._value
Bind sc._value to receive the owner’s current value:
@state.count.on_change.partial(sc._value)
def _(value):
print(value)
This is shorthand for calling .get() inside the handler. It is
meaningful only on a signal whose owner holds a value: a Property has
one, a plain Button does not.
sc._undefined
A Property declared without a default starts as sc._undefined:
class State(sc.State):
selection: sc.Property[str]
sc._undefined is falsy. Two consequences matter:
sc.binddoes not run its transform while the source still holdssc._undefined, because the transform may not accept it.- A bound target that needs a defined starting value should be given one explicitly:
label = sc.bind(state.selection, str.upper, default='')
Test for it with identity:
if state.selection.get() is sc._undefined:
...
What a handler should do
A handler is the equivalent of the body of an if st.button(...) branch,
but it runs only when that event happens. It should:
- update one or more properties;
- keep the work short;
- avoid touching the component tree (create components in
app(), not in a handler).
with sc.Button('Load') as button:
@button.on_click
def _():
state.status.set('loading')
data = load_data()
state.rows.set(data)
state.status.set('ready')
Reading a property gives its current value. Writing it schedules a patch.
Ordering and batching
During a single event, property writes are staged and flushed together. A derived property that depends on several sources is recomputed once, after the last declared source has settled. This keeps a multi-field update from producing a chain of intermediate renders.
Client-side interactions
Not every interaction needs the server. The following are local to the browser:
- opening and closing an
Expander; - switching
Tabs; - opening and closing a
Popover(the server can still callclose()); - folding a folder row in a tree;
- highlighting a row.
The server is told only about the results it needs: a new value, a
submitted path, a closed dialog.
Summary
| Concept | Role |
|---|---|
sc.run | Starts the persistent runtime |
| Component tree | The UI, built once per session |
Property | A reactive value |
Signal | An event with no value |
| Handler | The code that reacts to one event |
| Delta patch | The small update sent to the browser |
sc.bind / sc.bbind | Derived and two-way values |
Session State
State is the base class for application state. It is a
PropertyHost: a plain object whose fields are reactive Property and
Signal handles.
Declaring state
The simplest form declares fields inside __init__:
import streamlit_canary as sc
class State(sc.State):
def __init__(self):
super().__init__()
self.count = sc.Property(0)
self.name = sc.Property('')
state = State()
A field may also be declared by annotation. PropertyHost builds one
handle per instance:
class State(sc.State):
count: sc.Property[int] = 0
name: sc.Property[str]
reloaded: sc.Signal
Property[int]with a class-body value uses that value as the default.- Without a class-body value the property starts as
sc._undefined(see Special markers). Signaldeclares no payload;Signal[int]declares one positional payload.
Do not declare a Property instance at class level unless the state class
has a single instance. Class-level properties are shared between
instances.
Reading and writing
state.count.get() # 0
state.count.set(1) # 1, emits state.count.on_change
state['count'] # 1
state['count'] = 2 # 2
state['on_count'] # state.count.on_change
state.count += 1 is not supported by the property object itself. Use
the subscript form, which the runtime maps to get and set:
state['count'] += 1
Reacting to changes
@state.count.on_change
def _():
print('count is now', state.count.get())
A handler may receive the property handle or its current value:
@state.count.on_change.partial(sc._self)
def _(prop):
print(prop.get())
@state.count.on_change.partial(sc._value)
def _(value):
print(value)
Binding
A derived value is created with sc.bind:
double = sc.bind(state.count, lambda n: n * 2)
A two-way binding is created with sc.bbind:
sc.NumberInput('Count', sc.bbind(state.count))
See The Event Driven System for the full
explanation of triggers, multiple sources, and default=.
Versioning
State tracks a schema version:
class State(sc.State):
__version__ = 2
...
or pass it explicitly:
state = State(version=2)
The version is currently metadata (state.version). It gives the app a
place to detect an incompatible state layout and rebuild it.
Lifetime
State lives in the server process for the life of a session. Because the app function does not rerun, a value written in one handler is still there in the next handler.
This also means that state is not rebuilt from the script. If you need a value to reset, reset it explicitly:
state.count.set(0)
Relationship to Streamlit’s session_state
State replaces st.session_state for V3 apps. The two are not
synchronized. A V3 app should keep its state in State and pass it to
handlers by closure, or store it on the app object.
API Reference
This section documents the Components V3 library, grouped the way Streamlit groups its own API reference.
Each page follows the same shape:
- what the component is for;
- its constructor arguments;
- its reactive properties and signals;
- a short example.
Pages
- Text Elements
- Data Elements
- Chart Elements
- Button
- Inputs
- Layout
- Status Elements
- Media Elements
- Page Config
- Trees
Common keywords
All components accept width, height, max_width, min_width,
max_height, min_height, and visible, unless a page says otherwise.
Widgets also accept key where a stable identity is useful.
sc.Button('Go', width='stretch', visible=state.ready)
Text-like components default to visible='auto', which hides them while
their content is blank.
Reactive fields
A property is read and written through the same interface everywhere:
comp.value.get()
comp.value.set(new_value)
comp['value']
comp['value'] = new_value
comp['on_value']
Text Elements
Text elements display read-only content. They share a reactive text
field and default to visible='auto': empty content hides the element.
Text
Plain text. It is rendered literally: markdown is not parsed, and whitespace is preserved.
with sc.Text('Click count: 0') as txt:
@state.count.on_change.partial(sc._self)
def _(count):
txt.text.set('Click count: {}'.format(count.get()))
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | The content |
font_family | str | Optional font override |
font_size | str | Optional size override |
visible | bool | Property | 'auto' | Default 'auto' |
Properties: text.
Use Text when the content must never be interpreted as markdown, for
example a path, a file name, or user input.
Markdown
Renders markdown. The source is stored in a data-md attribute and parsed
in the browser by the bundled markdown-it, so formatting, icons, and
colors stay consistent with the rest of the page.
sc.Markdown('**Ready** in :green[3] seconds')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Markdown source |
visible | bool | Property | 'auto' | Default 'auto' |
Properties: text.
Streamlit extensions are supported:
:color[text], for example:red[error];:material/icon_name:, for material icons;- emoji shortcodes such as
:smile:; - effect markers
:bold[x],:italic[x],:strike[x],:underline[x].
The source is dedented and stripped before rendering, so a triple-quoted block can be indented naturally.
Title
A section heading.
sc.Title('Configuration')
sc.Title('Centered', horizontal_alignment='center')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Heading text |
horizontal_alignment | 'left' | 'center' | 'right' | Default 'left' |
help | str | Optional tooltip |
font_family | str | Optional font override |
font_size | str | Optional size override |
visible | bool | Property | 'auto' | Default 'auto' |
Title(' ') (exactly one space) is the supported way to reserve an empty
heading line.
PageTitle
A Title that also configures the page. It writes the text into the
document title and accepts the page-level options.
sc.PageTitle('My App', layout='wide', default_theme='dark')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Page title |
layout | 'centered' | 'wide' | Page width |
default_theme | 'light' | 'dark' | Theme |
font_family | str | Page font family |
dunder_literal | bool | Treat __x__ as literal text |
PageTitle is a canary extra. For page-level configuration without a
visible heading, use PageConfig.
Caption
Small, muted text.
sc.Caption('Last updated: 2026-10-19')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Caption text |
visible | bool | Property | 'auto' | Default 'auto' |
Code
A code block. Canary renders it as plain monospace text; there is no syntax highlighting.
sc.Code('print("hello")', language='python')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Code text |
language | str | Language label, default 'python' |
visible | bool | Property | 'auto' | Default 'auto' |
Fonts and alignment
Pass font_family=sc.MONOSPACE when a column of text must line up with
code. Monospace also defaults the size one step down, because the same
pixel size looks larger in a monospaced face. Pass font_size to override
that.
Data Elements
Table
A static table. rows is a sequence of rows, and the first cell of each
row is rendered as a row header (<th scope="row">).
sc.Table(
rows=[
('Alice', 'Engineering', 'alice@example.com'),
('Bob', 'Design', 'bob@example.com'),
],
header=('Name', 'Team', 'Email'),
caption='Team directory',
footer='3 people',
)
| Argument | Type | Meaning |
|---|---|---|
rows | sequence of sequences | Body rows |
header | sequence of strings | Optional header row |
caption | str | Caption above the table |
footer | str | Footer below the table |
header_background | bool | Draw a background behind the header |
title | str | Optional table title |
visible | bool | Property | 'auto' | Default 'auto' |
Properties: rows.
The table is static in the DOM: to update it, set a new rows value. The
row header cell is useful for a matrix-like table where the first column
labels the row.
Markdown color markers such as :red[overdue] work inside cells.
An empty rows value hides the table when visible='auto'.
Chart Elements
AltairChart
Renders an Altair / Vega-Lite chart.
import altair as alt
chart = alt.Chart(data).mark_bar().encode(
x='category',
y='value',
)
sc.AltairChart(chart)
| Argument | Type | Meaning |
|---|---|---|
chart | Altair chart or dict | The chart specification |
width | size keyword | Chart width |
visible | bool | Property | Default True |
Properties: chart.
AltairChart accepts either an Altair chart object (anything with a
to_dict() method) or a plain Vega-Lite dictionary. Altair itself is an
optional dependency: the component does not import it.
To update a chart, call set_chart() or set the chart property with a
new specification:
with sc.AltairChart(chart) as view:
@state.data.on_change
def _():
view.set_chart(alt.Chart(state.data.get()).mark_line())
Button
Button-style widgets trigger actions. They have no value of their own,
except for ToggleButton.
Button
with sc.Button('Save', type='primary') as button:
@button.on_click
def _():
save()
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Button text |
type | 'secondary' | 'primary' | Visual style, default 'secondary' |
help | str | Property | Markdown tooltip |
width | size keyword | Default 'content' |
enabled | bool | Property | Default True |
on_click | callable | Optional handler, attached at construction |
Properties: text, type, enabled, help.
Signals: on_click.
type, enabled, and help are reactive, so a button can change
appearance or availability as state changes:
sc.Button(
'Deploy',
enabled=sc.bind(state.ready, lambda x: x),
)
IconButton
A button that contains only an icon. A bare material icon name is wrapped
as :material/<name>: automatically.
sc.IconButton('refresh', help='Reload the list')
| Argument | Type | Meaning |
|---|---|---|
icon | str | Property | Icon name or short label |
help | str | Tooltip; recommended |
Other arguments are inherited from Button.
MenuButton
A button that opens a menu of options. Choosing a row sets value and
closes the menu, without a server round trip.
menu = sc.MenuButton('Export', options=('CSV', 'JSON'))
@menu.value.on_change
def _():
export_as(menu.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Trigger label |
options | sequence | Property | Menu rows |
value | any | Current choice |
format | callable or sequence | Row labels |
panel_max_height | int | Scroll the menu past this height |
Properties: text, options, value.
ToggleButton
A canary extra: a button that stays lit while its value is true.
with sc.ToggleButton('Bold') as bold:
@bold.value.on_change
def _():
apply_style(bold.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Button text |
value | bool | Property | Initial on / off state |
help, width, enabled | as Button | |
on_click | callable | Runs after value flips |
Properties: value, and a read-only type derived from value
('primary' while on, 'secondary' while off).
ToggleButton does not accept a type argument. Writing type raises an
error, so the look cannot disagree with the state.
Inputs
Input widgets hold a value that the user can change. Every widget in this section follows the same property model:
with sc.Checkbox('Enabled', value=True) as box:
@box.value.on_change
def _():
print(box.value.get())
Shared arguments
| Argument | Meaning |
|---|---|
label | Text drawn above the widget; bindable |
label_visibility | 'auto' (default), 'visible', 'hidden', 'collapsed' |
help | Markdown tooltip next to the label |
enabled | Whether the widget accepts input; bindable |
width | Size keyword, usually defaulting to 'stretch' |
label_visibility='auto' behaves like 'visible' when the label has
content and like 'collapsed' when it is empty. This is decided when the
page is rendered; a later change to label does not re-evaluate it.
Option widgets
CheckGroup, RadioGroup, Selectbox, Multiselect,
SegmentedControl, and SelectSlider share an options model:
options: the choices, bindable;index: the selected position, orNone;value: the selected value (a list for multi-select widgets);format: a callablevalue -> text, or a list of labels parallel tooptions.
index and value are kept in sync. Setting either one updates the
other.
sc.Selectbox(
'Level',
options=(1, 2, 3),
value=2,
format=lambda n: 'Level {}'.format(n),
)
Submitting text
TextInput, TextArea, and NumberInput expose:
on_value: the committed value changed (on blur or Enter);on_submit: Enter was pressed (Ctrl+Enter forTextArea);on_editing_finished: editing ended, either by submit or by losing focus.
TextInput additionally exposes on_editing, which fires on every
keystroke while the text is still uncommitted.
Pages
- Checkbox and Group
- Radiobox and Group
- Selectbox and Multiselect
- NumberInput
- TextInput and TextArea
- Toggle
- SegmentedControl and SelectSlider
- ReducibleGroup
Checkbox and Group
Checkbox
A single tick box.
with sc.Checkbox('Enable caching', value=True) as box:
@box.value.on_change
def _():
cache_enabled.set(box.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
value | bool | Property | Checked state, default False |
label_visibility | str | See Inputs |
Properties: label, value.
Signals: value.on_change (box['on_value']).
CheckGroup
A group of tick boxes for choosing several values.
with sc.CheckGroup(
'Extras',
options=('Deps', 'Docs', 'Tests'),
value=('Deps', 'Tests'),
) as group:
@group.on_change
def _():
print(group.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence, dict, or Property | Choices |
value | sequence | Property | Ticked values |
format | callable or sequence | Labels |
horizontal | bool | Lay out in a row |
max_height | int | Cap and scroll the list |
enabled | bool | Property | Whether input is accepted |
box_disabled | callable | Predicate for rows that cannot be ticked |
label_visibility | str | See Inputs |
Properties: label, options, value, focused_index.
Signals: on_change (same as value.on_change), options.on_change.
Dictionary form
Passing a dictionary spells each option’s starting state:
sc.CheckGroup('Extras', options={'Deps': True, 'Docs': False})
The keys become options, and value becomes a list of booleans parallel
to options, not a subset of them. This is useful when a caller wants to
keep the flags in a fixed order.
Focused row
focused_index is the row the client last clicked, or -1. It can drive
an action that applies to the highlighted row:
sc.Button(
'Open',
enabled=sc.bind(group.focused_index, lambda i: i >= 0),
)
Radiobox and Group
RadioGroup
A group of radio buttons for choosing exactly one value.
with sc.RadioGroup(
'Project scope',
options=('All', 'Current', 'Selected'),
value='Current',
) as radio:
@radio.value.on_change
def _():
scope.set(radio.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence | Property | Choices |
index | int | Selected position |
value | any | Selected value |
format | callable or sequence | Labels |
horizontal | bool | Lay out in a row |
max_height | int | Cap and scroll the list |
enabled | bool | Property | Whether input is accepted |
box_disabled | callable | Predicate for rows that cannot be selected |
label_visibility | str | See Inputs |
Properties: label, options, index, value, focused_index.
Signals: value.on_change (radio['on_value']), options.on_change.
Radio = RadioGroup is an alias, matching the name used in some
Streamlit examples.
Choosing between RadioGroup and Selectbox
Use RadioGroup when every option should be visible at once and the list
is short. Use Selectbox when the list is long or the
space is tight.
Disabled rows
box_disabled marks rows that are visible but cannot be selected:
sc.RadioGroup(
'Target',
options=('dev', 'prod'),
box_disabled=lambda x: x == 'prod',
)
The tree panels use this for the .. navigation row, which is a target
for a click but not a value.
Selectbox and Multiselect
Selectbox
A dropdown for choosing one value.
with sc.Selectbox(
'Choose a branch',
options=('main', 'dev', 'release'),
value='main',
) as box:
@box.value.on_change
def _():
branch.set(box.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence | Property | Choices |
index | int | Selected position |
value | any | Selected value |
format | callable or sequence | Labels |
placeholder | str | Property | Hint while nothing is picked |
accept_new_option | bool | Offer an input row in the dropdown |
format_new_option | callable | Convert typed text into a value |
take_new_option | callable | Handle typed text yourself |
truncate_start | bool | Show the tail of an overlong label |
enabled | bool | Property | Whether input is accepted |
label_visibility | str | See Inputs |
Properties: label, options, index, value, placeholder.
Signals: value.on_change, options.on_change.
Accepting a new option
sc.Selectbox(
'Channel',
options=('A', 'B'),
accept_new_option=True,
)
A submitted value that is not already an option is added at the front and
selected. format_new_option converts the text before it is stored:
sc.Selectbox(
'Port',
options=(80, 443),
accept_new_option=True,
format_new_option=lambda x: int(x, 0),
)
take_new_option replaces that behavior entirely. Nothing is added to
options; the callable receives the text and decides what to do. This is
what a path bar uses.
Width
With width='content', the trigger is as wide as the widest option, not
as wide as the current value. Switching options therefore does not make
the control jump in size.
Multiselect
A dropdown for choosing several values.
with sc.Multiselect(
'Select chart results',
options=('Droop', 'PSD', 'SNDR'),
value=('Droop', 'PSD'),
) as box:
@box.value.on_change
def _():
print(box.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence | Property | Choices |
value | sequence | Property | Selected values |
format | callable or sequence | Labels |
placeholder | str | Property | Hint while nothing is selected |
height | 'fixed' | size keyword | Default 'fixed' |
label_visibility | str | See Inputs |
Properties: label, options, value, placeholder.
Signals: value.on_change.
Selection order
The selection keeps the order in which rows were ticked. That order is
what the trigger shows, what value reports, and what the server stores.
Ticking adds to the end; unticking removes from the list.
Height
height='fixed' keeps the trigger on one line and scrolls the summary
sideways. This avoids the control growing taller as more options are
picked. Pass a size keyword to override it.
NumberInput
A box for entering a number.
with sc.NumberInput('Workers', value=4, min_value=1, step=1) as box:
@box.value.on_change
def _():
workers.set(box.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
value | int | float | Property | Current number |
min_value | int | float | Inclusive lower bound |
max_value | int | float | Inclusive upper bound |
step | int | float | Stepper increment, default 0 |
format | callable | Display formatter (for example hex) |
placeholder | str | Property | Hint while empty |
label_visibility | str | See Inputs |
Properties: label, value, placeholder.
Signals: value.on_change, on_submit, on_editing_finished.
Integer versus float
The initial value decides the widget’s numeric type:
sc.NumberInput('Count', value=0) # int widget
sc.NumberInput('Ratio', value=0.0) # float widget
An int widget requires int bounds and an int step. A float widget also accepts ints and converts them to float.
The client sends the text; the server parses it with the widget’s type. An
int widget therefore accepts both '41' and '0x29'.
Stepper
step=0 (the default) shows no stepper. A positive value adds + / -
controls. If the stepper does not fit the available width, it degrades to
a vertical form and is eventually dropped.
Signals
on_value: the committed value changed.on_submit: Enter was pressed; carries the raw text.on_editing_finished: editing ended (submit or blur); carries the raw text.
A submit emits both on_submit and on_editing_finished, in that order.
Validation
Constructing a widget with an out-of-range value raises ValueError.
Passing a bound property that later falls out of range leaves the
validation to your handler.
TextInput and TextArea
TextInput
A single-line text box.
with sc.TextInput('Project name', value='') as box:
@box.on_submit
def _(text):
create_project(text)
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
value | str | Property | Current text |
placeholder | str | Property | Hint while empty |
enabled | bool | Property | Whether input is accepted |
help | str | Tooltip |
input_history | sequence, Property, or None | Suggestions panel |
accept_new_option | bool | Add an “Add: …” row to the panel |
truncate_start | bool | Show the tail of an overlong value |
label_visibility | str | See Inputs |
Properties: label, value, placeholder, enabled, input_history.
Signals: value.on_change, on_editing, on_submit,
on_editing_finished.
value is committed on blur or Enter. on_editing reports every
keystroke, which is how a caller can follow text before it is committed.
Input history
input_history adds a caret that opens a list of suggestions:
sc.TextInput('File', input_history=('/tmp/a.txt', '/tmp/b.txt'))
None: no caret.- An empty sequence: a caret that is disabled.
- A non-empty sequence: a working panel.
Picking a row fills the box. The text remains freely editable, so this is also a way to get “selectbox with free text”.
accept_new_option=True adds an “Add: …” row for the current text.
Truncating from the start
truncate_start=True keeps the end of an overlong value in view. Use it
for paths, where the last segment is the part that distinguishes one value
from another.
TextArea
A multi-line text box. It has the same fields and signals as
TextInput, except on_editing, input_history, accept_new_option,
and truncate_start.
with sc.TextArea('Notes', height=160) as box:
@box.on_submit
def _(text):
save_notes(text)
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
value | str | Property | Current text |
placeholder | str | Property | Hint while empty |
height | size keyword | Default 200 |
enabled | bool | Property | Whether input is accepted |
help | str | Tooltip |
label_visibility | str | See Inputs |
Press Ctrl+Enter to submit. Plain Enter inserts a newline. The box scrolls when the text overflows its height.
Toggle
A sliding on / off switch.
with sc.Toggle('Auto refresh', value=True) as toggle:
@toggle.value.on_change
def _():
auto_refresh.set(toggle.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
value | bool | Property | On / off state, default False |
label_visibility | str | See Inputs |
Properties: label, value.
Signals: value.on_change (toggle['on_value']).
Toggle is an alias of ToggleBox. Its fields match Checkbox; only the
visual differs.
Toggle versus ToggleButton
ToggleBox(Toggle) is an input widget: it belongs among other form controls and has a label above it.ToggleButtonis a button that stays lit. It belongs in a toolbar, and itstypeis derived from its state.
SegmentedControl and SelectSlider
Both widgets choose one value from a fixed list, but present it differently.
SegmentedControl
A row of pills inside one rounded track.
with sc.SegmentedControl(
'View',
options=('List', 'Grid', 'Tree'),
value='List',
) as control:
@control.on_switch
def _(value):
change_view(value)
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence | Property | Choices |
index | int | Selected position |
value | any | Selected value |
format | callable or sequence | Labels |
label_visibility | str | See Inputs |
Properties: label, options, index, value.
Signals: on_switch (the new value), value.on_change.
on_switch fires only for a user click. A programmatic value.set or
index.set stays silent, so a listener can tell a click apart from a
refresh.
The track hugs its options by default (width='content'). Pass
width='stretch' to fill the parent.
Only single selection is implemented.
SelectSlider
A slider whose ticks are a fixed set of options.
with sc.SelectSlider(
'Start level',
options=range(15, 0, -1),
value=15,
format=lambda x: 'Lv.{}'.format(x),
) as slider:
@slider.value.on_change
def _():
start_level.set(slider.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
options | sequence | Property | Discrete choices |
index | int | Selected position |
value | any | Selected value |
format | callable or sequence | Labels |
label_visibility | str | See Inputs |
Properties: label, options, index, value.
Signals: value.on_change.
Every option is a tick on one track. Clicking or dragging to a tick selects it; the interaction is client-side and the result comes back as a change event. The slider fills its parent by default.
ReducibleGroup
A list of items, each of which can be removed from its own trailing x.
with sc.ReducibleGroup('Files', options=('a.txt', 'b.txt')) as group:
@group.on_reduce
def _(item):
print('removed', item)
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Optional label |
options | sequence | Property | The items |
format | callable or sequence | Labels |
label_visibility | str | See Inputs |
Properties: label, options.
Signals: on_reduce (the dropped item), options.on_change.
The rows are laid out like a MenuButton’s menu. Hovering a row reveals
the x; clicking it removes the item from options and emits
on_reduce.
The component updates its own options list. Your handler receives the
dropped item so it can keep an external list in sync.
@group.on_reduce
def _(item):
state.files.set([x for x in state.files.get() if x != item])
Layout
Layout components decide where other components go. They are used as context managers; every component created inside the block becomes a child.
with sc.Row('center'):
sc.Text('Left')
sc.Button('Right')
Pages
- Row — horizontal layout
- Column — vertical layout with a flex weight
- Container — bordered, sized, scrollable box
- Expander — collapsible section
- Grid — explicit rows and columns
- Tabs — tab bar with one panel per label
- Dialog — modal dialog
- Popover — floating panel behind a trigger
- Floating — sticky corner cluster
- Space — flexible spacer
- Bottom — bottom-anchored container
Spacing
The layout uses two spacing tokens:
--st-hgap(8px) for horizontal gaps;--st-vgap(16px) for vertical gaps.
A Row spaces its children with the horizontal gap and uses the vertical
gap when it wraps. A Container and a Grid use the vertical gap between
their children / rows.
Sizing
Layout components accept the shared size keywords. A Container with a
fixed height scrolls when its content overflows. height='stretch'
fills the parent instead; inside a Row, it makes the column span the
row’s tallest sibling.
Row
A horizontal layout container.
with sc.Row('center'):
sc.IconButton('home')
sc.Space()
sc.Button('Save')
| Argument | Type | Meaning |
|---|---|---|
vertical_alignment | 'top' | 'center' | 'bottom' | Cross-axis alignment, default 'top' |
hide_when_empty | bool | Hide the row when every child is hidden |
width, height, … | size keywords | The row’s own size |
Properties: visible.
Alignment
vertical_alignment decides how children line up when they differ in
height:
with sc.Row('bottom'):
sc.Text('Label')
sc.TextInput('Value')
Wrapping
A Row lays children out left to right and wraps when there is not enough
width. The horizontal gap is used between items on one line; the vertical
gap is used between wrapped lines.
hide_when_empty
A row that exists only to be filled later still costs its parent one gap,
because a zero-height box is still a box. Set hide_when_empty=True to
hide the row while all of its children are hidden:
with sc.Row(hide_when_empty=True):
sc.Button('Retry', visible=state.failed)
This is decided at render time. A child that a later patch turns visible does not bring the row back, because patches reach the child, not its parent.
Column
A vertical layout container inside a Row. Column is an alias of
Container, so it takes every Container argument plus weight.
with sc.Row():
with sc.Column(weight=5):
sc.Text('Main pane')
with sc.Column(weight=2):
sc.Text('Side pane')
| Argument | Type | Meaning |
|---|---|---|
weight | float | Flex-grow ratio inside a Row |
width, height, … | size keywords | The column’s own size |
border | bool | Draw a border |
border_title | str | Property | Title in the border |
animated | bool | Animate height when visible flips |
Properties: border_title, visible.
Weights
weight decides how the leftover width is shared. A (5, 2) split is two
columns with weight=5 and weight=2. Without weights, columns share the
width equally.
with sc.Row():
sc.Column(weight=3)
sc.Column(weight=1)
A column is a full Container; see Container for
borders, fixed heights, and scrolling.
Container
A vertical box for grouping components. Column is an alias of
Container.
with sc.Container(border=True, border_title='Settings', height=320):
sc.Checkbox('Enable caching')
sc.NumberInput('Workers', value=4)
| Argument | Type | Meaning |
|---|---|---|
width | size keyword | Width |
weight | float | Flex-grow ratio inside a Row |
border | bool | Draw a frame |
border_title | str | Property | Title drawn in the top border |
height | size keyword | Fixed height; content scrolls |
animated | bool | Animate the height when visible flips |
visible | bool | Property | Default True |
Properties: border_title, visible.
Scrolling
A fixed height caps the box and makes it scroll:
with sc.Container(height=240):
for line in lines:
sc.Text(line)
max_height has the same effect. min_height does not.
Stretching inside a row
height='stretch' makes the container fill the parent’s height. Inside a
Row, it spans the row’s tallest sibling:
with sc.Row():
with sc.Container(height='stretch'):
sc.Text('Left')
with sc.Container(height='stretch'):
sc.Text('Right')
Showing and hiding
visible controls whether the container is rendered. animated=True
transitions the height instead of making it pop:
with sc.Container(visible=state.show_advanced, animated=True):
sc.Text('Advanced options')
The container stays in the tree either way, so a later patch can bring it back.
Border title
border_title is only honored with border=True. It is drawn inline in
the frame, the way a <legend> sits in a fieldset.
Expander
A collapsible section with a clickable header.
with sc.Expander('Configurations', expanded=True):
sc.Checkbox('Enable caching')
sc.Button('Force refresh')
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Header text |
expanded | bool | Whether the body starts open |
visible | bool | Property | Whether the expander is shown |
Properties: label, visible.
Expanding and collapsing is handled entirely in the browser, so it never reaches the server and never triggers a rerun.
Dynamic header
label is bindable:
sc.Expander(sc.bind(state.file_count, lambda n: 'Files ({})'.format(n)))
Hiding
visible=False removes the whole expander, header included. To keep the
header but hide the body, use a Container with animated=True and drive
its visible from a ToggleButton.
Grid
A layout with an explicit number of rows and columns.
with sc.Grid(rows=1, cols=2) as grid:
with next(grid):
sc.Button('A')
with next(grid):
sc.Button('B')
| Argument | Type | Meaning |
|---|---|---|
rows | int | Number of rows, default 1 |
cols | int or sequence of floats | Column count or weights, default 2 |
columns | same as cols | Alias kept for compatibility |
vertical_alignment | 'top' | 'center' | 'bottom' | Cell content alignment |
Properties: visible.
Addressing cells
Three forms are available:
# 1. Sequential
with sc.Grid(rows=2, cols=2) as grid:
with next(grid):
sc.Text('row 0, col 0')
with next(grid):
sc.Text('row 0, col 1')
# 2. By coordinate, reusable
with grid[1, 0]:
sc.Text('row 1, col 0')
# 3. Iteration
for cell in grid:
with cell:
sc.Text('...')
next(grid) and for cell in grid walk left to right, then top to
bottom. grid[row, col] returns a stable Cell; accessing the same
coordinate again reuses it.
Column weights
cols accepts a sequence of weights, mirroring st.columns((5, 2)):
with sc.Grid(cols=(5, 2)) as grid:
...
Alignment
vertical_alignment decides how a cell’s content sits in its row when the
columns differ in height.
Grid flow
A grid defines its rows and columns up front. It is not a masonry layout;
use nested Row / Column structures when the content should determine
its own height.
Tabs
A tab bar with one panel per label.
with sc.Tabs(('Overview', 'Details')) as tabs:
with tabs['Overview']:
sc.Text('Summary')
with tabs['Details']:
sc.Text('Full details')
| Argument | Type | Meaning |
|---|---|---|
labels | sequence of strings | Tab labels |
active | str | Property | Visible tab, default the first |
Properties: active.
Signals: active.on_change (tabs['on_active']).
Addressing panels
with tabs['Overview']:
...
# or in label order
with sc.Tabs(('a', 'b')) as tabs:
with next(tabs):
...
with next(tabs):
...
tabs[label] returns that label’s panel, creating it on first access and
reusing it afterwards. Panels are built up front; switching tabs is
entirely client-side.
Label collisions
A label wins over property access. A tab named active is not reachable
as tabs['active']; read the property through tabs.active instead.
Programmatic switching
tabs.active.set('Details')
The client switches to that panel on the next patch.
Dialog
A modal dialog.
with sc.Dialog('Select a file', visible=state.browsing) as dialog:
sc.Text('...')
sc.Button('Close', on_click=lambda: dialog.visible.set(False))
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Dialog title |
width | 'small' | 'medium' | 'large' | int | Panel width |
visible | bool | Property | Whether the dialog is open |
max_height | int | Cap the panel height; the panel scrolls |
min_height | int | Floor the panel height |
Properties: text, visible.
Signals: on_close.
Opening and closing
A V3 dialog is an ordinary component built once. Unlike st.dialog, it
does not decorate a function and it does not rerun when opened. Set
visible to open or close it:
state.browsing.set(True) # open
state.browsing.set(False) # close
visible may be bound directly at construction, as in the example above.
Width presets
| Value | Width |
|---|---|
'small' | 500px (default) |
'medium' | 750px |
'large' | 1280px |
int | Explicit pixels |
Client dismissal
The close button, a click on the backdrop, and Escape all dismiss the dialog. When that happens:
visibleis set toFalse;on_closeis emitted.
@dialog.on_close
def _():
state.browsing.set(False)
The first step already closes the dialog; the signal lets your app clean up or log the dismissal.
Height
max_height and min_height apply to the panel, not to the full-screen
backdrop. A capped panel scrolls as a whole.
Popover
A trigger button that reveals a floating panel.
with sc.Popover('Export requirements'):
sc.RadioGroup('Mirror source', options=('PyPI', 'Local'))
sc.Checkbox('Lock self', value=True)
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Trigger label |
width | size keyword | Trigger width, default 'content' |
help | str | Property | Tooltip on the trigger |
enabled | bool | Property | Whether the trigger accepts clicks |
panel_align | 'trigger' | 'row' | 'above' | Panel placement |
panel_max_height | int | Cap the panel height; it scrolls |
visible | bool | Property | Whether the trigger is shown |
Properties: text, visible.
Signals: on_open, on_close.
Placement
'trigger'(default): the panel opens under the trigger.'row': the panel stretches across the surroundingRow.'above': the panel opens above the trigger. Use this when the trigger sits at the bottom of a scrollable box, where a downward panel would be clipped.
Opening and closing
Open and close are handled in the browser and do not reach the server. Two signals report the ends:
@popover.on_open
def _():
refresh_preview()
@popover.on_close
def _():
clear_preview()
Calling popover.close() asks the client to fold the panel away while
leaving the trigger in place. This is how a widget such as
SingleTreeSelect dismisses its own panel after a Confirm.
Server-side state
The open / closed state lives in the browser; the server cannot read it.
Use on_open and on_close when work should happen only while the panel
is up.
Floating
A container that sticks to a corner of its parent’s visible area.
with sc.Popover('Browse', panel_max_height=420):
with sc.FloatingContainer('top-right'):
sc.IconButton('refresh')
sc.RadioGroup('Folder contents', options=('a.txt', 'b.txt'))
with sc.FloatingContainer('bottom-right'):
sc.Button('Confirm', type='primary')
| Argument | Type | Meaning |
|---|---|---|
position | 'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right' | Required |
width, height, … | size keywords | See Container |
Properties: border_title, visible.
Floating is an alias of FloatingContainer.
How it behaves
- Children lay out in a row; the cluster hugs them.
- The box sticks to its corner of the parent’s visible area, so content scrolls underneath while the box stays put.
- It keeps its place in the layout (
position: sticky, not an overlay), so the parent needs no extra padding.
Where it may go
A floating cluster must sit directly in a vertical layout:
- a
Container, - a
Popover, - a
Dialog, - or the app root.
Anything else raises ValueError. A Row cannot hold one, because the
corner’s horizontal half comes from align-self, which a row would fight.
Authoring order
Sticky positioning can only offset a box from its own flow position:
- author a
top-*cluster as the parent’s first child; - author a
bottom-*cluster as the parent’s last child.
A bottom cluster also receives margin-top: auto, so it drops to the end
of the remaining room.
Space
A flexible spacer.
with sc.Row():
sc.IconButton('home')
sc.Space(size='stretch')
sc.Popover(':material/bucket_check: 0')
| Argument | Type | Meaning |
|---|---|---|
size | size keyword | Alias of width |
width | size keyword | The spacer’s width |
Properties: visible.
Inside a Row, a stretching Space absorbs the leftover width, pushing
everything after it to the right edge. In a vertical container there is no
leftover width to absorb, so it renders nothing.
size is the spelling used by st.space. Space(size='stretch') and
Space(width='stretch') are the same spacer, and passing both raises an
error.
Bottom
A container that is pushed to the bottom of its parent layout.
with sc.Container(height=320):
sc.Text('Content')
with sc.BottomContainer():
sc.Button('Close')
| Argument | Type | Meaning |
|---|---|---|
width, height, … | size keywords | See Container |
visible | bool | Property | Whether it is shown |
Properties: border_title, visible.
Bottom is an alias of BottomContainer.
Inside a container
The parent pushes the bottom container down, so a card can keep its actions at the bottom and a dialog can pin a button to its base.
At the app root
At the app root, a bottom container becomes a dock:
- it hangs 16px above the window’s bottom edge;
- it stays there while the page scrolls;
- the page reserves the dock’s height at its end, so the last content can still be scrolled clear of it.
This is the same idea as st.bottom, but a canary BottomContainer is
allowed in any layout, not only at the root.
Status Elements
Status elements report what the app is doing. Most of them are reactive: update the text or value and the browser follows.
Callouts
Info, Success, Warning, and Error are one alert box in four
palettes. Callout is the shared base.
sc.Info('Loading the project...')
sc.Success('Saved.')
sc.Warning('This overwrites the existing file.')
sc.Error('Could not connect to the server.')
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Message; :color[..] markup allowed |
visible | bool | Property | 'auto' | Default 'auto' |
Properties: text, visible.
With visible='auto', a blank message draws nothing, and a non-blank one
shows. Pass a bound value to drive visibility yourself:
sc.Error(state.error, visible=sc.bind(state.error, bool))
Spinner
A spinner indicator. It is hidden by default and is usually driven with a
with block or a bound flag.
spinner = sc.Spinner()
with spinner('Syncing...'):
do_work()
| Argument | Type | Meaning |
|---|---|---|
text | str | Property | Label next to the ring |
visible | bool | Property | Default False |
Properties: text, visible; method reset(text='').
Calling the spinner sets its text and returns the spinner, so
with spinner('Syncing...') reads naturally. Entering shows it; exiting
restores the visibility it had before.
A state-driven form:
sc.Spinner(state.busy_text, visible=sc.bind(state.busy_text, bool))
Progress
A progress bar.
with sc.Progress(total=len(sheets)) as progress:
for sheet in sheets:
progress.update(sheet.title)
| Argument | Type | Meaning |
|---|---|---|
value | int | None | Property | Percentage, or None for indeterminate |
text | str | Property | Caption under the bar |
label_visibility | str | How the caption row is drawn |
visible | bool | Property | Default False |
total | int | Steps used by update() |
auto_close | bool | Hide the bar when the with block ends |
Properties: value, text, visible; attributes total, index.
Methods:
update(item=None): advance one step and set the caption;close(): hide the bar, keeping its value;reset(text='', value=0): put it back to idle.
Use the with form inside a handler, where the block spans the work.
At build time the block would be the build itself and would close the bar
immediately.
Toast
A stack of transient notifications at the page’s top-right.
toast.show('Saved!', icon=':material/check:')
Properties: messages (a list of dictionaries).
Methods:
show(text, icon='', duration='short');clear().
Durations follow Streamlit: 'short' (4s), 'long' (10s), 'infinite',
or a positive number of seconds. Up to five messages are visible. Hovering
the stack fans it out and pauses the countdowns; each message also has a
close button.
Most apps do not declare a Toast. sc.toast(...) reaches the page’s
single stack, which the runtime creates if needed.
LogPanel
A live view of what the app prints to the terminal.
with sc.BottomContainer():
sc.LogPanel(source='stdout')
| Argument | Type | Meaning |
|---|---|---|
source | 'stdout' | 'stderr' | Stream to follow |
border_title | str | Property | Border title, default 'LOG' |
placeholder | str | Property | Hint while there are no lines |
visible | bool | Property | 'auto' | Default 'auto' |
height | size keyword | Default 200 |
Properties: lines, border_title, placeholder, visible.
Every line written to source lands in the panel, newest at the bottom.
The buffer keeps the last 500 lines; the oldest are dropped. ANSI color
codes are stripped.
push_divider(title='') inserts a rule, which is useful for marking the
boundary between two runs:
panel.push_divider('12:00:00')
A clear button floats in the top-right corner while there is something to clear.
With visible='auto', an empty panel is hidden. A placeholder counts as
content, so a panel with only a hint keeps its frame.
Media Elements
PdfViewer
Displays a PDF in the page using the bundled pdf.js.
sc.PdfViewer('manual.pdf')
sc.PdfViewer('https://example.com/report.pdf')
| Argument | Type | Meaning |
|---|---|---|
src | str | bytes | A local path, a URL, or PDF bytes |
pages_to_render | sequence of ints | Pages to draw; empty means all |
height | size keyword | Default 500 |
max_height | int | Cap the viewer height |
visible | bool | Property | 'auto' | Default 'auto' |
Properties: src, visible.
src may be:
- a filesystem path;
- an HTTP(S) URL;
- raw PDF bytes.
pages_to_render limits which pages are drawn. This is useful for a large
document where only a few pages matter.
If only max_height is given, the height becomes 'content' and the
viewer grows up to that cap. An empty src hides the viewer when
visible='auto'.
Page Config
PageConfig
Configures the page without rendering a visible element.
sc.PageConfig(
title='My App',
layout='wide',
default_theme='dark',
)
| Argument | Type | Meaning |
|---|---|---|
title | str | Document title |
layout | 'centered' | 'wide' | Page width |
default_theme | 'light' | 'dark' | Theme |
font_family | str | Page font family |
dunder_literal | bool | Treat __x__ as literal text |
Every argument defaults to None, which means “leave this setting
alone”. Page settings are written into a single global configuration;
later writers override earlier ones item by item.
PageConfig renders nothing. It is the class form of the older
sc.set_page_config(...), which still works but is deprecated.
PageTitle
PageTitle is Title and PageConfig combined. It draws a visible page
heading and also sets the document title and page options.
sc.PageTitle('My App', layout='wide', default_theme='dark')
Use PageTitle when the app should show a heading. Use PageConfig when
the page needs configuration but no visible title.
dunder_literal
Markdown normally reads __text__ as bold. Real content often contains
dunder names such as __init__ or __name__, which then turn bold by
accident.
Set dunder_literal=True to treat __x__ as literal text. Bold remains
available as **x** or :bold[x].
sc.PageConfig(dunder_literal=True)
This is an experimental switch and is off by default.
Document title updates
The first render sets the browser tab title from the page configuration.
After that, changing the text of a PageTitle patches the document title
as well. A title element is treated as plain text for this purpose, while
its visible heading still renders markdown.
Trees
The tree components browse a folder hierarchy. They are canary extras: Streamlit has no equivalent.
There are four panels, all built on the same row and selection engine:
| Component | Shape |
|---|---|
SingleTreeSelect | One folder at a time; .. walks up |
TreeView | Every level in one listing over a static data model |
FileTreeView | The same classic tree, rooted on a real folder |
DualTreeSelect | Two columns: subfolders left, files right |
ColumnView | Finder-style stack of columns, one level per column |
On top of the panels there are path inputs:
| Component | Shape |
|---|---|
PathInput | A text box that resolves its text into a path |
PathSelect | PathInput plus a navigation ladder |
PathInputBrowse | A path box paired with one of the panels |
Shared selection modes
The single-list, classic, and file trees share three modes:
'single': one node;'multiple': several nodes within the folder being browsed; leaving the folder drops them;'multicross': nodes gathered across folders into a bucket.
A tuple of modes, such as ('single', 'multicross'), shows a mode switch.
selection_mode decides which modes are offered; initial_mode decides
which one opens.
DualTreeSelect and ColumnView are single-select only.
Shared row gestures
- Clicking a row picks or ticks it.
- Hovering a folder reveals an
->arrow; clicking it walks into the folder. - The
..row walks up. Its checkbox is frozen because it is a navigation target, not a node.
Filtering
filter keeps files by suffix:
sc.FileTreeView(filter='.py')
sc.FileTreeView(filter=('.py', '.pyi'))
Folders are always listed.
Pages
- SingleTreeSelect
- TreeView
- FileTreeView
- DualTreeSelect
- ColumnView
- PathInput and PathSelect
- PathInputBrowse
SingleTreeSelect
The single-pane folder browser: one folder on show at a time, with .. to
walk up.
with sc.Popover('Browse', panel_align='row', panel_max_height=500):
tree = sc.SingleTreeSelect(filter='.txt')
path = tree.value.get()
Layout:
[ /current/folder v ] [home] [refresh] [bucket] [mode] <- toolbar
.. (go to parent)
subfolder/ ->
another-file.txt
[ Confirm ]
Arguments
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label above the panel |
start_directory | str | Folder to open, default the cwd |
home_directory | str | Where home returns; defaults to start_directory |
filter | str or tuple | Suffixes to keep |
height | int | Cap the listing height and scroll |
width | size keyword | Panel width |
selection_mode | str or tuple | 'single', 'multiple', 'multicross', 'none' |
initial_mode | str | Which offered mode opens |
show_confirm_button | bool | Default True |
border | bool | Draw a frame; defaults to standalone |
label_visibility | str | See Inputs |
help | str | Property | Tooltip |
leading and trailing are builders run at the head and tail of the
toolbar row. They are used by wrappers to put their own widgets around the
actions, not usually by an app.
Properties
value: the selection.'single': one path, or''.- Multi modes: a list of paths.
mode: the active mode.directory: the folder on show (read-only).rows_visible: whether the listing is drawn.
Prefer select() and clear() over writing value directly; those keep
the listing in step.
Signals
value.on_change: the selection changed.on_navigate(directory): the listing moved to a new folder.on_submit(paths): Confirm was clicked; carries the resolved paths.
Methods
reload(): re-read the current folder from disk.select(path): add a path that is not in the listing.clear(): drop the selection.resolve(): the paths Confirm reports.open_path(path): move the panel onto what a path names.
Widgets
tree.widgets exposes two sub-components:
bottom_bar: theRowunder the listing. Enter it to append actions after the Confirm button.confirm_button: the button itself, reachable even when hidden.
with tree.widgets['bottom_bar']:
sc.Button('Open input folder')
Selection modes
'single'picks one node.'multiple'holds picks within the folder being browsed. Walking away drops them.'multicross'gathers picks into a bucket across folders. Only a mode set containing it draws the bucket.'none'draws no boxes; the rows are read-only.
A tuple draws a mode switch, but only when it contains more than one mode.
TreeView
A classic tree over a static data model: every level lives in one listing, and folders fold open in place.
with sc.TreeView('Distribution', {
'A': {
'B': None, # a leaf
'C': {}, # an empty folder
'D': {'E': None, 'G': {}},
}
}) as tree:
...
node = tree.value.get()
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
data | dict | The model (bindable) |
filter | str or tuple | Suffixes to keep |
start_directory, height, selection_mode, … | see SingleTreeSelect |
Properties: value, mode, directory, rows_visible, data.
Signals: value.on_change, on_navigate, on_submit.
The data model
data is one map from a name to a sub-map or None:
- a
dictvalue is a folder;{}is an empty folder; Noneis a leaf;- the map must have exactly one key, which is the root.
Node paths are the names joined with /, for example A/D/G. This is what
the location bar shows, and what value contains.
An empty model ({} or None) lists no rows. A model can be handed in
later.
Re-rooting
data is bindable. Setting a new model re-roots the tree and clears the
selection, because the old paths belong to the old model:
tree.data.set(new_model)
The new model is validated on assignment, so a malformed one raises instead of drawing nothing.
Folding
A folder with children carries a chevron to the left of its row. Clicking it folds the folder open in place; clicking the row itself picks or ticks it. The two gestures are independent.
Multi-select
In the multi modes a folder stands for its whole subtree:
- ticking a folder drops any picks it covers, then adds it;
- once every child is picked, the picks fold into the folder itself;
- unticking a row inside a pick opens the pick one level at a time and leaves that row out.
A folder with only part of its subtree picked draws a half-ticked box.
Root behavior
.. is drawn only after leaving the root. Standing on the root, there is
no outside to walk to.
FileTreeView
The classic cascading tree, backed by the real filesystem.
with sc.FileTreeView('Source', start_directory='./data') as tree:
...
picked = tree.value.get()
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
start_directory | str | Property | Folder to open; '.' and '' mean the cwd |
root_directory | str | Property | Top the panel may not pass; default '/' |
home_directory | str | Where home returns |
filter | str or tuple | Suffixes to keep |
height, selection_mode, … | see SingleTreeSelect |
Properties: value, mode, directory, rows_visible,
start_directory, root_directory.
Signals: value.on_change, on_navigate, on_submit.
Methods: open_path(path), reload(), select(path), clear(),
resolve().
start_directory
The folder the panel opens on. It is bindable: a caller that owns the value can move the panel by setting it.
tree.start_directory.set('C:/work/project')
root_directory
The top the panel may not pass. '/' means the whole machine.
When confined:
- the location ladder starts at the root;
- drives and the root’s own ancestors are not offered;
..disappears while the panel stands on the root.
start_directory must be the root or somewhere under it. A violation
raises ValueError.
Location ladder
With no real root, the ladder lists the machine’s drives followed by the ancestors of the folder on show, itself last:
['C:/', 'D:/', 'E:/', 'C:/Likianta', ..., <current folder>]
Every parent, and every other drive, is one pick away.
Paths
Every row is a real file or folder, listed on demand. value and
resolve() return absolute paths.
DualTreeSelect
A two-column browser: subfolders on the left, files on the right.
with sc.DualTreeSelect('data/sample', filter='.txt') as tree:
@tree.on_confirm
def _():
path = tree.value.get()
| Argument | Type | Meaning |
|---|---|---|
start_directory | str | Folder to open, default the cwd |
filter | str or tuple | Suffixes to keep |
height | int | Height of both columns, default 500 |
node_type | 'file' | 'folder' | What may be selected, default 'file' |
preview_subfolders | bool | Also list the highlighted subfolder’s contents |
show_confirm_button | bool | Default True |
width | size keyword | Panel width |
Properties: value (the highlighted path), directory (the folder the
left column is on).
Signals: on_confirm.
Methods: reload().
Left column
The left column navigates subfolders:
- back;
- forward (enter the highlighted subfolder);
- refresh;
- create a new folder.
The “Current location” bar at the top lists every ancestor, so any parent is one pick away.
Right column
The right column lists the files of the current folder, or the files of
the highlighted subfolder when preview_subfolders=True. A single click on
a subfolder previews it.
value follows the highlighted row, so it is live rather than waiting for
Confirm.
Confirm
With show_confirm_button=True, the Confirm button is disabled while
nothing is selected. Clicking it emits on_confirm.
Differences from the other panels
- It is single-select only; there are no modes.
- It has no
selection_mode,label, orhelparguments. - Its toolbar is its own (back / forward / refresh / new folder), so
PathInputBrowseuses it only in the popup style.
ColumnView
A Finder-style stack of columns, one level per column.
with sc.ColumnView('Pick a file', 'data/sample') as tree:
...
path = tree.value.get()
Layout:
[ /current/folder v ] [home] [refresh] <- toolbar
+---------+ +---------+ +---------+
| alpha/ | | one.py | | a.txt |
| beta/ | | two.py | | b.txt |
+---------+ +---------+ +---------+
| Argument | Type | Meaning |
|---|---|---|
label | str | Property | Label |
start_directory | str | Folder the first column opens on |
filter | str or tuple | Suffixes to keep |
height | int | Height of every column, default 500 |
max_columns | int | Columns on show at once, default 4 |
home_directory | str | Where home returns |
accept_new_option | bool | Offer a typed path in the location bar |
show_location | bool | Draw the location ladder |
show_confirm_button | bool | Default True |
rows_visible | bool | Property | Whether the columns are drawn |
label_visibility | str | See Inputs |
help | str | Property | Tooltip |
Properties: value, mode (always 'single'), directory (the folder
the rightmost column shows).
Signals: on_navigate(directory), on_submit(paths).
Methods: reload(), select(path), clear(), resolve().
Interaction
- Clicking a folder opens its contents in a new column to the right.
- Clicking a file marks it.
- Clicking a folder in a column further left replaces everything to its right, so the whole trail stays on show.
- The panel needs no “go up” row; the trail is the way back.
value
Single-select. It names the node picked in the rightmost column, or that
column’s folder while nothing inside is picked. A column view always has a
current folder, so value is never empty.
Windowing
At most max_columns columns are on show. Walking deeper scrolls the
earlier ones out; the window keeps the last max_columns folders, so the
column just opened stays visible. The location ladder and home still
reach any ancestor in one pick.
Toolbar
The toolbar holds the location ladder, home, and refresh. With
leading / trailing, a wrapper can add its own widgets around them.
PathInput and PathSelect
PathInput
A text input whose text is resolved into a path.
with sc.PathInput('Output folder', 'C:/work') as box:
@box.on_submit
def _(text):
use_path(box.value.get())
| Argument | Type | Meaning |
|---|---|---|
label | str | Label |
value | str | Initial text |
width | size keyword | Width |
input_history | sequence, Property, or None | Suggestions |
accept_new_option | bool | Add an “Add: …” row |
Properties: value, input_history.
Signals: on_submit, on_editing_finished (both carry the text as typed).
Method: show(path), which makes path the value and rewrites the box
even when it already shows that path in another spelling (backslashes, a
trailing separator, a ..).
Path resolution
- Blank text resolves to
''. - Text that names something on disk resolves to an absolute path with forward slashes.
- Text that names nothing yet is kept, with only its separators straightened. A path that does not exist is still a path, and the caller decides what it means.
History
input_history is both a suggestion list and a memory:
None: no caret, no panel.- A literal sequence: seeds a memory. Every non-empty path the box holds joins it (newest first, no duplicates). Capacity is at least 20; a larger seed rounds up to the next ten. The panel lists the memory in alphabetical order.
- A
Property: the caller owns the list; nothing is remembered.
PathSelect
A PathInput with a navigation ladder.
with sc.PathSelect(
'Go to',
'.',
ladder=['C:/', 'C:/work', 'C:/work/app'],
) as box:
...
path = box.value.get()
| Argument | Type | Meaning |
|---|---|---|
ladder | sequence or Property | Rungs; the current folder is last |
Properties: value, input_history, ladder.
The box looks the same as PathInput, but has two dropdowns:
- clicking the input area opens the ladder: drives, then the ancestors of the folder on show. The last rung is drawn as selected.
- clicking the caret opens
input_history. The caret is greyed out while the history is empty.
Both close on a click outside or Escape. PathSelect takes no
accept_new_option, because clicking the box already opens the ladder,
and typing plus Enter already resolves the path.
The ladder is fed by the host, since the box does not know which folder it is “in”.
PathInputBrowse
A path box paired with one of the tree panels.
with sc.PathInputBrowse(
'Select a file',
'data/sample',
filter='.txt',
) as browse:
...
path = browse.value.get()
Arguments
| Argument | Type | Meaning |
|---|---|---|
label | str | Path box label |
start_directory | str | Path shown and folder opened |
panel_style | 'popup' | 'fold' | 'inline' | Where the panel lives |
expanded | bool | Whether a foldable panel starts open |
tree_style | 'single_list' | 'tree_view' | 'dual_pane' | 'column_view' | Which panel |
filter | str or tuple | Suffixes to keep |
initial_mode | str | Mode to open in |
selection_mode | str or tuple | Modes offered |
input_history | sequence, Property, or None | Seed for the box’s history |
node_type | 'file' | 'folder' | Dual-pane only |
panel_height | int | Cap on the panel height |
width | size keyword | Width |
Properties: value, mode, directory, input_history.
start_directory
'.'(the default): the cwd for both the box and the panel.'': the panel opens on the cwd, but the box is blank (“nothing picked yet”).- Any other value: shown in the box. If it names a file, the panel opens on its folder; otherwise on the value itself.
Panel styles
'popup' (default)
The panel lives in a Browse popover with its own toolbar and a Confirm
button.
- Confirm closes the popover and writes the choice into the box.
- The choice is the panel’s pick, or, with nothing ticked, the folder the panel ended on.
- The choice also joins the selection, so
valuecarries it.
Only this style accepts tree_style='dual_pane', because the dual-pane
panel has its own toolbar and is not a good fit for the inline toolbar.
'fold'
The panel sits under the box and is unfolded by a fold button.
- No popover, no Confirm.
- The fold button shows and hides the rows.
- The box follows the panel’s pick live.
'inline'
The panel sits under the box and is always on show.
Toolbar takeover
In the fold and inline styles, the path box leads the panel’s toolbar row
and the fold button (fold style only) trails it. The panel’s own location
bar is turned off, because the box carries the ladder. home, refresh,
the bucket, and the mode control sit between them.
The box used here is a PathSelect, so clicking it
opens the navigation ladder and clicking the caret opens its history.
tree_style
| Value | Panel |
|---|---|
'single_list' | SingleTreeSelect |
'tree_view' | FileTreeView |
'dual_pane' | DualTreeSelect (popup only) |
'column_view' | ColumnView |
Typed paths
A path typed into the box and submitted moves the panel onto the folder that holds it, and joins the panel’s selection. A path that does not exist names no folder, so the panel is left where it was.
Writes made by the wrapper itself are silenced, so a pick in the panel does not echo back as a typed path and pull the panel away.
Legacy Streamlit Wrappers (V1)
Caution
The V1 wrappers are no longer exported as
sc.*(they left the top-level namespace in 0.5.0). Import them fromstreamlit_canary.legacy_streamlit_wrappers. For new work, read Components.
Components V1 is the original layer of Streamlit Canary. It is built directly on top of Streamlit and follows the same rerun model: every interaction re-executes the script, and widgets return their current value from that run.
They are still there, under their own module:
from streamlit_canary import legacy_streamlit_wrappers as legacy
value = legacy.toggle_button('Show details')
It remains useful for:
- small scripts that do not need persistent state;
- porting an existing Streamlit app with minimal changes;
- comparing behavior against upstream Streamlit.
It is not the recommended way to build a new app. The rerun model is exactly what V3 removes, and V1 widgets cannot participate in the V3 component tree or its delta patches.
Related pages
Migrating to the default components
V1 and the event-driven components can live in the same process, but they do
not share a component tree. An app should use only sc.* components inside
its app function.
The mental model
V1:
import streamlit as st
count = st.session_state.get('count', 0)
if st.button('Increase'):
count += 1
st.session_state.count = count
st.text('Click count: {}'.format(count))
V3:
import streamlit_canary as sc
class State(sc.State):
count = sc.Property(0)
state = State()
def app():
with sc.Text('Click count: 0') as txt:
@state.count.on_change.partial(sc._self)
def _(cnt):
txt.text.set('Click count: {}'.format(cnt.get()))
with sc.Button('Increase') as btn:
@btn.on_click
def _():
state['count'] += 1
The differences:
- The app function runs once. There is no
if st.button(...)branch that runs on the next rerun. - State is explicit.
Statefields arePropertyobjects, not dictionary entries. - Widgets expose properties and signals. You attach a handler with
@widget.on_clickor@widget.value.on_changeinstead of reading a return value. - Updates are pushed. Changing
txt.textsends a patch; the browser replaces that text node.
A practical checklist
- Replace
st.session_statewith aStatesubclass. - Replace
st.button(...)plus anifbranch withsc.Button(...)plus@button.on_click. - Replace “read the widget’s return value” with
widget.value.get(). - Replace “write a widget’s value by assigning its return value” with
widget.value.set(...). - Move conditional rendering into a
Container(visible=...)or anExpander, rather than skipping code on a rerun. - Replace
st.rerun()with a state change. A rerun is neither needed nor available in the V3 runtime.
Things that do not translate one to one
- A V3 component is created once. If a widget should not exist at all, keep
it hidden with
visible=Falseinstead of not creating it. - Layout is built with context managers (
with sc.Row(): ...), not by assigning columns to variables. - Server-side code inside a handler runs on a worker thread. Keep handlers short, and do not block the event loop for long periods.
Roadmap
This page lists the work that is currently planned for Streamlit Canary. It is indicative rather than a promise, and the order may change.
Stabilization
- Finish the remaining widget parity with Streamlit.
- Harden the patch protocol and reconnect / resync behavior.
- Add more automated tests for the runtime, the renderer, and the trees.
API consolidation
- Port Streamlit’s chart elements to sc.
- Drop the deprecated
sc.v3.*alias (0.5.0 made the components the defaultsc.*API and retired the V1 wrappers tostreamlit_canary.legacy_streamlit_wrappers).
Documentation
- Complete the API reference for every V3 component.
- Add a migration guide for larger real-world apps.
- Publish runnable examples for the most common layouts and widgets.
Developer experience
- Improve error messages when a component is used outside a valid parent.
- Document the sizing and visibility rules in one place.
- Provide a small project template that can be copied to start a new app.
Packaging and release
- Keep the published package in step with the repository.
- Add a
LICENSEfile and make the license visible on GitHub. - Automate the release checklist (tests, version bump, changelog, tag).
Applications
- Rewrite a few third-party Streamlit apps with V3 components to validate the API against real use cases.
- Publish the rewritten apps as reference implementations.