Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

StreamlitStreamlit Canary (V3)
Execution modelThe script reruns on every interactionThe app function runs once
UI updatesA new render tree is diffed on every rerunA delta patch updates the affected DOM nodes
Statest.session_state is rebuilt on each rerunState holds Property objects for the session
WidgetsReturn values from the last runExpose reactive Property fields and Signals
Cost of one clickProportional to the whole scriptProportional 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.bind and sc.bbind build derived and two-way values, on_change and 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 native pyapp-window window, 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=(),
)
ArgumentTypeMeaning
targetcallable or strThe app function, a .py path, or a module path
portintPort to listen on, default 3001
blockingboolWhether run() waits for the app to end
hoststr or NoneInterface to bind
show_windowboolOpen the app in a native window
window_sizetuple or keywordWindow size in CSS pixels
window_pospositionWindow position, default 'center'
window_titlestrWindow title
window_iconstrWindow icon path
icon / titlestrAliases of window_icon / window_title
extra_argssequence of strExtra 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

Targetblocking=Trueblocking=False
CallableServes in the current thread until stoppedServes on a daemon thread and returns
Path / moduleWaits for the child processLeaves 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.

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

  1. Build the tree once. The app function passed to sc.run is called once. Components created inside it stay alive for the session.
  2. 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:

ModuleComponents
textsText, Markdown, Title, PageTitle, Caption, Code
dataTable
chartsAltairChart
buttonsButton, IconButton, MenuButton, ToggleButton
inputsCheckbox, CheckGroup, Radio, RadioGroup, Selectbox, Multiselect, NumberInput, TextInput, TextArea, Toggle, SegmentedControl, SelectSlider, ReducibleGroup
layoutsRow, Column, Container, Expander, Grid, Tabs, Dialog, Popover, FloatingContainer, Space, BottomContainer
statusInfo, Success, Warning, Error, Callout, Spinner, Progress, Toast, LogPanel
mediaPdfViewer
pagePageConfig
treesSingleTreeSelect, 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: an int in CSS pixels, 'stretch', 'content', or 'auto' (where the component supports it).
  • max_width / min_width / max_height / min_height: positive integers or None.

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.bind does not run its transform while the source still holds sc._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 call close());
  • 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

ConceptRole
sc.runStarts the persistent runtime
Component treeThe UI, built once per session
PropertyA reactive value
SignalAn event with no value
HandlerThe code that reacts to one event
Delta patchThe small update sent to the browser
sc.bind / sc.bbindDerived 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).
  • Signal declares 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

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()))
ArgumentTypeMeaning
textstr | PropertyThe content
font_familystrOptional font override
font_sizestrOptional size override
visiblebool | 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')
ArgumentTypeMeaning
textstr | PropertyMarkdown source
visiblebool | 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')
ArgumentTypeMeaning
textstr | PropertyHeading text
horizontal_alignment'left' | 'center' | 'right'Default 'left'
helpstrOptional tooltip
font_familystrOptional font override
font_sizestrOptional size override
visiblebool | 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')
ArgumentTypeMeaning
textstr | PropertyPage title
layout'centered' | 'wide'Page width
default_theme'light' | 'dark'Theme
font_familystrPage font family
dunder_literalboolTreat __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')
ArgumentTypeMeaning
textstr | PropertyCaption text
visiblebool | 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')
ArgumentTypeMeaning
textstr | PropertyCode text
languagestrLanguage label, default 'python'
visiblebool | 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',
)
ArgumentTypeMeaning
rowssequence of sequencesBody rows
headersequence of stringsOptional header row
captionstrCaption above the table
footerstrFooter below the table
header_backgroundboolDraw a background behind the header
titlestrOptional table title
visiblebool | 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)
ArgumentTypeMeaning
chartAltair chart or dictThe chart specification
widthsize keywordChart width
visiblebool | PropertyDefault 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()
ArgumentTypeMeaning
labelstr | PropertyButton text
type'secondary' | 'primary'Visual style, default 'secondary'
helpstr | PropertyMarkdown tooltip
widthsize keywordDefault 'content'
enabledbool | PropertyDefault True
on_clickcallableOptional 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')
ArgumentTypeMeaning
iconstr | PropertyIcon name or short label
helpstrTooltip; recommended

Other arguments are inherited from Button.

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())
ArgumentTypeMeaning
labelstr | PropertyTrigger label
optionssequence | PropertyMenu rows
valueanyCurrent choice
formatcallable or sequenceRow labels
panel_max_heightintScroll 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())
ArgumentTypeMeaning
labelstr | PropertyButton text
valuebool | PropertyInitial on / off state
help, width, enabledas Button
on_clickcallableRuns 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

ArgumentMeaning
labelText drawn above the widget; bindable
label_visibility'auto' (default), 'visible', 'hidden', 'collapsed'
helpMarkdown tooltip next to the label
enabledWhether the widget accepts input; bindable
widthSize 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, or None;
  • value: the selected value (a list for multi-select widgets);
  • format: a callable value -> text, or a list of labels parallel to options.

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 for TextArea);
  • 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

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())
ArgumentTypeMeaning
labelstr | PropertyLabel
valuebool | PropertyChecked state, default False
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence, dict, or PropertyChoices
valuesequence | PropertyTicked values
formatcallable or sequenceLabels
horizontalboolLay out in a row
max_heightintCap and scroll the list
enabledbool | PropertyWhether input is accepted
box_disabledcallablePredicate for rows that cannot be ticked
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence | PropertyChoices
indexintSelected position
valueanySelected value
formatcallable or sequenceLabels
horizontalboolLay out in a row
max_heightintCap and scroll the list
enabledbool | PropertyWhether input is accepted
box_disabledcallablePredicate for rows that cannot be selected
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence | PropertyChoices
indexintSelected position
valueanySelected value
formatcallable or sequenceLabels
placeholderstr | PropertyHint while nothing is picked
accept_new_optionboolOffer an input row in the dropdown
format_new_optioncallableConvert typed text into a value
take_new_optioncallableHandle typed text yourself
truncate_startboolShow the tail of an overlong label
enabledbool | PropertyWhether input is accepted
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence | PropertyChoices
valuesequence | PropertySelected values
formatcallable or sequenceLabels
placeholderstr | PropertyHint while nothing is selected
height'fixed' | size keywordDefault 'fixed'
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
valueint | float | PropertyCurrent number
min_valueint | floatInclusive lower bound
max_valueint | floatInclusive upper bound
stepint | floatStepper increment, default 0
formatcallableDisplay formatter (for example hex)
placeholderstr | PropertyHint while empty
label_visibilitystrSee 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)
ArgumentTypeMeaning
labelstr | PropertyLabel
valuestr | PropertyCurrent text
placeholderstr | PropertyHint while empty
enabledbool | PropertyWhether input is accepted
helpstrTooltip
input_historysequence, Property, or NoneSuggestions panel
accept_new_optionboolAdd an “Add: …” row to the panel
truncate_startboolShow the tail of an overlong value
label_visibilitystrSee 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)
ArgumentTypeMeaning
labelstr | PropertyLabel
valuestr | PropertyCurrent text
placeholderstr | PropertyHint while empty
heightsize keywordDefault 200
enabledbool | PropertyWhether input is accepted
helpstrTooltip
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
valuebool | PropertyOn / off state, default False
label_visibilitystrSee 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.
  • ToggleButton is a button that stays lit. It belongs in a toolbar, and its type is 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)
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence | PropertyChoices
indexintSelected position
valueanySelected value
formatcallable or sequenceLabels
label_visibilitystrSee 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())
ArgumentTypeMeaning
labelstr | PropertyLabel
optionssequence | PropertyDiscrete choices
indexintSelected position
valueanySelected value
formatcallable or sequenceLabels
label_visibilitystrSee 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)
ArgumentTypeMeaning
labelstr | PropertyOptional label
optionssequence | PropertyThe items
formatcallable or sequenceLabels
label_visibilitystrSee 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')
ArgumentTypeMeaning
vertical_alignment'top' | 'center' | 'bottom'Cross-axis alignment, default 'top'
hide_when_emptyboolHide the row when every child is hidden
width, height, …size keywordsThe 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')
ArgumentTypeMeaning
weightfloatFlex-grow ratio inside a Row
width, height, …size keywordsThe column’s own size
borderboolDraw a border
border_titlestr | PropertyTitle in the border
animatedboolAnimate 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)
ArgumentTypeMeaning
widthsize keywordWidth
weightfloatFlex-grow ratio inside a Row
borderboolDraw a frame
border_titlestr | PropertyTitle drawn in the top border
heightsize keywordFixed height; content scrolls
animatedboolAnimate the height when visible flips
visiblebool | PropertyDefault 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')
ArgumentTypeMeaning
labelstr | PropertyHeader text
expandedboolWhether the body starts open
visiblebool | PropertyWhether 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')
ArgumentTypeMeaning
rowsintNumber of rows, default 1
colsint or sequence of floatsColumn count or weights, default 2
columnssame as colsAlias 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')
ArgumentTypeMeaning
labelssequence of stringsTab labels
activestr | PropertyVisible 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))
ArgumentTypeMeaning
textstr | PropertyDialog title
width'small' | 'medium' | 'large' | intPanel width
visiblebool | PropertyWhether the dialog is open
max_heightintCap the panel height; the panel scrolls
min_heightintFloor 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

ValueWidth
'small'500px (default)
'medium'750px
'large'1280px
intExplicit pixels

Client dismissal

The close button, a click on the backdrop, and Escape all dismiss the dialog. When that happens:

  1. visible is set to False;
  2. on_close is 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)
ArgumentTypeMeaning
labelstr | PropertyTrigger label
widthsize keywordTrigger width, default 'content'
helpstr | PropertyTooltip on the trigger
enabledbool | PropertyWhether the trigger accepts clicks
panel_align'trigger' | 'row' | 'above'Panel placement
panel_max_heightintCap the panel height; it scrolls
visiblebool | PropertyWhether 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 surrounding Row.
  • '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')
ArgumentTypeMeaning
position'top-left', 'top-center', 'top-right', 'bottom-left', 'bottom-center', 'bottom-right'Required
width, height, …size keywordsSee 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')
ArgumentTypeMeaning
sizesize keywordAlias of width
widthsize keywordThe 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')
ArgumentTypeMeaning
width, height, …size keywordsSee Container
visiblebool | PropertyWhether 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.')
ArgumentTypeMeaning
textstr | PropertyMessage; :color[..] markup allowed
visiblebool | 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()
ArgumentTypeMeaning
textstr | PropertyLabel next to the ring
visiblebool | PropertyDefault 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)
ArgumentTypeMeaning
valueint | None | PropertyPercentage, or None for indeterminate
textstr | PropertyCaption under the bar
label_visibilitystrHow the caption row is drawn
visiblebool | PropertyDefault False
totalintSteps used by update()
auto_closeboolHide 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')
ArgumentTypeMeaning
source'stdout' | 'stderr'Stream to follow
border_titlestr | PropertyBorder title, default 'LOG'
placeholderstr | PropertyHint while there are no lines
visiblebool | Property | 'auto'Default 'auto'
heightsize keywordDefault 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')
ArgumentTypeMeaning
srcstr | bytesA local path, a URL, or PDF bytes
pages_to_rendersequence of intsPages to draw; empty means all
heightsize keywordDefault 500
max_heightintCap the viewer height
visiblebool | 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',
)
ArgumentTypeMeaning
titlestrDocument title
layout'centered' | 'wide'Page width
default_theme'light' | 'dark'Theme
font_familystrPage font family
dunder_literalboolTreat __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:

ComponentShape
SingleTreeSelectOne folder at a time; .. walks up
TreeViewEvery level in one listing over a static data model
FileTreeViewThe same classic tree, rooted on a real folder
DualTreeSelectTwo columns: subfolders left, files right
ColumnViewFinder-style stack of columns, one level per column

On top of the panels there are path inputs:

ComponentShape
PathInputA text box that resolves its text into a path
PathSelectPathInput plus a navigation ladder
PathInputBrowseA 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

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

ArgumentTypeMeaning
labelstr | PropertyLabel above the panel
start_directorystrFolder to open, default the cwd
home_directorystrWhere home returns; defaults to start_directory
filterstr or tupleSuffixes to keep
heightintCap the listing height and scroll
widthsize keywordPanel width
selection_modestr or tuple'single', 'multiple', 'multicross', 'none'
initial_modestrWhich offered mode opens
show_confirm_buttonboolDefault True
borderboolDraw a frame; defaults to standalone
label_visibilitystrSee Inputs
helpstr | PropertyTooltip

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: the Row under 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()
ArgumentTypeMeaning
labelstr | PropertyLabel
datadictThe model (bindable)
filterstr or tupleSuffixes 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 dict value is a folder; {} is an empty folder;
  • None is 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()
ArgumentTypeMeaning
labelstr | PropertyLabel
start_directorystr | PropertyFolder to open; '.' and '' mean the cwd
root_directorystr | PropertyTop the panel may not pass; default '/'
home_directorystrWhere home returns
filterstr or tupleSuffixes 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()
ArgumentTypeMeaning
start_directorystrFolder to open, default the cwd
filterstr or tupleSuffixes to keep
heightintHeight of both columns, default 500
node_type'file' | 'folder'What may be selected, default 'file'
preview_subfoldersboolAlso list the highlighted subfolder’s contents
show_confirm_buttonboolDefault True
widthsize keywordPanel 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, or help arguments.
  • Its toolbar is its own (back / forward / refresh / new folder), so PathInputBrowse uses 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   |
+---------+ +---------+ +---------+
ArgumentTypeMeaning
labelstr | PropertyLabel
start_directorystrFolder the first column opens on
filterstr or tupleSuffixes to keep
heightintHeight of every column, default 500
max_columnsintColumns on show at once, default 4
home_directorystrWhere home returns
accept_new_optionboolOffer a typed path in the location bar
show_locationboolDraw the location ladder
show_confirm_buttonboolDefault True
rows_visiblebool | PropertyWhether the columns are drawn
label_visibilitystrSee Inputs
helpstr | PropertyTooltip

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())
ArgumentTypeMeaning
labelstrLabel
valuestrInitial text
widthsize keywordWidth
input_historysequence, Property, or NoneSuggestions
accept_new_optionboolAdd 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()
ArgumentTypeMeaning
laddersequence or PropertyRungs; 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

ArgumentTypeMeaning
labelstrPath box label
start_directorystrPath shown and folder opened
panel_style'popup' | 'fold' | 'inline'Where the panel lives
expandedboolWhether a foldable panel starts open
tree_style'single_list' | 'tree_view' | 'dual_pane' | 'column_view'Which panel
filterstr or tupleSuffixes to keep
initial_modestrMode to open in
selection_modestr or tupleModes offered
input_historysequence, Property, or NoneSeed for the box’s history
node_type'file' | 'folder'Dual-pane only
panel_heightintCap on the panel height
widthsize keywordWidth

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

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 value carries 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

ValuePanel
'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 from streamlit_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.

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:

  1. The app function runs once. There is no if st.button(...) branch that runs on the next rerun.
  2. State is explicit. State fields are Property objects, not dictionary entries.
  3. Widgets expose properties and signals. You attach a handler with @widget.on_click or @widget.value.on_change instead of reading a return value.
  4. Updates are pushed. Changing txt.text sends a patch; the browser replaces that text node.

A practical checklist

  • Replace st.session_state with a State subclass.
  • Replace st.button(...) plus an if branch with sc.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 an Expander, 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=False instead 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 default sc.* API and retired the V1 wrappers to streamlit_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 LICENSE file 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.