diff --git a/content/develop/concepts/custom-components/_index.md b/content/develop/concepts/custom-components/_index.md index fd0c24a3c..0585ba456 100644 --- a/content/develop/concepts/custom-components/_index.md +++ b/content/develop/concepts/custom-components/_index.md @@ -1,62 +1,52 @@ --- -title: Components +title: Custom Components slug: /develop/concepts/custom-components -description: Learn how to build and use custom Streamlit components to extend app functionality with third-party Python modules and custom UI elements. -keywords: custom components, third-party modules, component development, extend functionality, custom UI, component integration, Streamlit components +description: Learn about Streamlit custom components - powerful extensions that unlock capabilities beyond built-in widgets using web technologies. +keywords: custom components, component development, extend streamlit, web components, custom widgets, component architecture --- # Custom Components -Components are third-party Python modules that extend what's possible with Streamlit. +Custom Components are powerful extensions for Streamlit that unlock capabilities beyond the built-in widgets. They let you integrate any web technology—from advanced data visualizations to specialized input controls to complete mini-applications—directly into your Streamlit apps. -## How to use a Component +## Getting started -Components are super easy to use: + -1. Start by finding the Component you'd like to use. Two great resources for this are: - - The [Component gallery](https://streamlit.io/components) - - [This thread](https://discuss.streamlit.io/t/streamlit-components-community-tracker/4634), - by Fanilo A. from our forums. + -2. Install the Component using your favorite Python package manager. This step and all following - steps are described in your component's instructions. +

Overview of Custom Components

- For example, to use the fantastic [AgGrid - Component](https://github.com/PablocFonseca/streamlit-aggrid), you first install it with: +Learn what custom components are, when to use them, and understand the differences between v1 and v2 approaches. - ```python - pip install streamlit-aggrid - ``` +
-3. In your Python code, import the Component as described in its instructions. For AgGrid, this step - is: + - ```python - from st_aggrid import AgGrid - ``` +

Components v2

-4. ...now you're ready to use it! For AgGrid, that's: +The next generation of custom components with enhanced capabilities, bidirectional communication, and simplified development. - ```python - AgGrid(my_dataframe) - ``` +
-## Making your own Component + -If you're interested in making your own component, check out the following resources: +

Components v1

-- [Create a Component](/develop/concepts/custom-components/create) -- [Publish a Component](/develop/concepts/custom-components/publish) -- [Components API](/develop/concepts/custom-components/intro) -- [Blog post for when we launched Components!](https://blog.streamlit.io/introducing-streamlit-components/) +The original custom components framework. Learn how to use and build v1 components. -Alternatively, if you prefer to learn using videos, our engineer Tim Conkling has put together some -amazing tutorials: +
-##### Video tutorial, part 1 + - +

Publishing Components

-##### Video tutorial, part 2 +Learn how to package and distribute your custom components to the community. - +
+ +
+ +## Component gallery + +Explore the [Community Component Gallery](https://streamlit.io/components) to discover components built by the Streamlit community. diff --git a/content/develop/concepts/custom-components/components-v1/_index.md b/content/develop/concepts/custom-components/components-v1/_index.md new file mode 100644 index 000000000..f9a64644b --- /dev/null +++ b/content/develop/concepts/custom-components/components-v1/_index.md @@ -0,0 +1,62 @@ +--- +title: Components v1 +slug: /develop/concepts/custom-components/components-v1 +description: Learn how to build and use custom Streamlit components to extend app functionality with third-party Python modules and custom UI elements. +keywords: custom components, third-party modules, component development, extend functionality, custom UI, component integration, Streamlit components +--- + +# Custom Components + +Components are third-party Python modules that extend what's possible with Streamlit. + +## How to use a Component + +Components are super easy to use: + +1. Start by finding the Component you'd like to use. Two great resources for this are: + - The [Component gallery](https://streamlit.io/components) + - [This thread](https://discuss.streamlit.io/t/streamlit-components-community-tracker/4634), + by Fanilo A. from our forums. + +2. Install the Component using your favorite Python package manager. This step and all following + steps are described in your component's instructions. + + For example, to use the fantastic [AgGrid + Component](https://github.com/PablocFonseca/streamlit-aggrid), you first install it with: + + ```python + pip install streamlit-aggrid + ``` + +3. In your Python code, import the Component as described in its instructions. For AgGrid, this step + is: + + ```python + from st_aggrid import AgGrid + ``` + +4. ...now you're ready to use it! For AgGrid, that's: + + ```python + AgGrid(my_dataframe) + ``` + +## Making your own Component + +If you're interested in making your own component, check out the following resources: + +- [Create a Component](/develop/concepts/custom-components/components-v1/create) +- [Publish a Component](/develop/concepts/custom-components/publish) +- [Components API](/develop/concepts/custom-components/components-v1/intro) +- [Blog post for when we launched Components!](https://blog.streamlit.io/introducing-streamlit-components/) + +Alternatively, if you prefer to learn using videos, our engineer Tim Conkling has put together some +amazing tutorials: + +##### Video tutorial, part 1 + + + +##### Video tutorial, part 2 + + diff --git a/content/develop/concepts/custom-components/components-api.md b/content/develop/concepts/custom-components/components-v1/v1-component-api.md similarity index 99% rename from content/develop/concepts/custom-components/components-api.md rename to content/develop/concepts/custom-components/components-v1/v1-component-api.md index 6495ae67b..3c989cf28 100644 --- a/content/develop/concepts/custom-components/components-api.md +++ b/content/develop/concepts/custom-components/components-v1/v1-component-api.md @@ -1,6 +1,6 @@ --- title: Intro to custom components -slug: /develop/concepts/custom-components/intro +slug: /develop/concepts/custom-components/components-v1/intro description: Learn to develop Streamlit custom components with static and bi-directional communication between Python and JavaScript for extended functionality. keywords: custom component development, static components, bi-directional components, Python JavaScript communication, component API, component development --- diff --git a/content/develop/concepts/custom-components/create-component.md b/content/develop/concepts/custom-components/components-v1/v1-component-create.md similarity index 96% rename from content/develop/concepts/custom-components/create-component.md rename to content/develop/concepts/custom-components/components-v1/v1-component-create.md index 9c4a77328..3ca20016d 100644 --- a/content/develop/concepts/custom-components/create-component.md +++ b/content/develop/concepts/custom-components/components-v1/v1-component-create.md @@ -1,6 +1,6 @@ --- title: Create a Component -slug: /develop/concepts/custom-components/create +slug: /develop/concepts/custom-components/components-v1/create description: Step-by-step guide to creating custom Streamlit components from scratch, including setup, development environment, and component structure. keywords: create component, component development, component setup, development environment, component structure, custom component creation, build components --- diff --git a/content/develop/concepts/custom-components/limitations.md b/content/develop/concepts/custom-components/components-v1/v1-component-limitations.md similarity index 96% rename from content/develop/concepts/custom-components/limitations.md rename to content/develop/concepts/custom-components/components-v1/v1-component-limitations.md index f92c4df66..bf0fe8bbd 100644 --- a/content/develop/concepts/custom-components/limitations.md +++ b/content/develop/concepts/custom-components/components-v1/v1-component-limitations.md @@ -1,6 +1,6 @@ --- title: Limitations of custom components -slug: /develop/concepts/custom-components/limitations +slug: /develop/concepts/custom-components/components-v1/limitations description: Understand the limitations and constraints of Streamlit custom components including iframe restrictions and differences from base Streamlit functionality. keywords: component limitations, iframe restrictions, component constraints, custom component issues, component differences, development limitations --- diff --git a/content/develop/concepts/custom-components/components-v2/_index.md b/content/develop/concepts/custom-components/components-v2/_index.md new file mode 100644 index 000000000..7c689f212 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/_index.md @@ -0,0 +1,83 @@ +--- +title: Custom components v2 +slug: /develop/concepts/custom-components/components-v2 +description: Learn about Streamlit custom components v2 - the next generation framework with enhanced capabilities, bidirectional communication, and simplified development. +keywords: custom components v2, next generation components, bidirectional communication, enhanced capabilities, modern component development +--- + +# Custom components v2 + +Components v2 represents a reimagining of how custom components work in Streamlit. It's designed to unlock new capabilities and dramatically simplify the development experience. To view the command reference, see the [API Reference](/develop/api-reference/custom-components). + +## Getting started + + + + + +

Quickstart examples

+ +Get started quickly with practical examples showing interactive buttons, data exchange, and complete component implementations. + +
+ + + +

Component registration

+ +Define your component's structure with HTML, CSS, and JavaScript. + +
+ + + +

Component mounting

+ +Create instances of your component in your app and handle their output. + +
+ + + +

Bidirectional communication

+ +Exchange data between your component and Python. + +
+ + + +

State vs triggers

+ +Understand the two communication mechanisms for building interactive components. + +
+ + + +

Theming and styling

+ +Make your components look great with Streamlit's theme integration and CSS custom properties. + +
+ + + +

Package-based components

+ +Build distributable components with TypeScript, external dependencies, and the official component template. + +
+ +
+ +## Migration from v1 to v2 + +If you have existing v1 components, check out these migration examples: + +- [streamlit-bokeh v2 migration](https://github.com/streamlit/streamlit-bokeh/pull/40) +- [streamlit-pdf v2 migration](https://github.com/streamlit/streamlit-pdf/pull/25) + +## What's next? + +Ready to build your first v2 component? Start with the [Quickstart examples](/develop/concepts/custom-components/components-v2/examples) to see practical implementations, then learn about [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount) to understand the fundamentals. diff --git a/content/develop/concepts/custom-components/components-v2/communicate.md b/content/develop/concepts/custom-components/components-v2/communicate.md new file mode 100644 index 000000000..2ae0c3d76 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/communicate.md @@ -0,0 +1,140 @@ +--- +title: Bidirectional communication +slug: /develop/concepts/custom-components/components-v2/communicate +description: Learn how to exchange data between your custom v2 component and Python, including sending data to the frontend and receiving user interactions. +keywords: custom components v2, bidirectional communication, data exchange, setStateValue, setTriggerValue, callbacks, data parameter +--- + +# Bidirectional communication + +Custom components v2 supports full bidirectional communication between your Python backend and JavaScript frontend. This enables you to: + +- Send data from Python to your component via the `data` parameter +- Receive user actions in Python from your component's state and trigger values +- Create feedback loops where Python updates the component programmatically + +The basic concepts of component communication are introduced in the [Component mounting](/develop/concepts/custom-components/components-v2/mount) guide. After you understand the basics, read this guide to learn how to create feedback loops where Python updates the component programmatically. + + + +This guide explains how to recreate behavior that's similar to native Streamlit widgets: setting a component's state from Session State. In general, you don't need to pass a component's state back into itself. This is just one pattern to manage a component's state through Session State. + +However, because components can have multiple states and triggers, you must work with `st.session_state..` for custom components instead of simply `st.session_state.` like you do with native Streamlit widgets. + + + +## Prerequisites + +Before you read this guide, you should understand the following concepts: + +- [Component mounting](/develop/concepts/custom-components/components-v2/mount) +- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) +- [Widget behavior](/develop/concepts/architecture/widget-behavior) + +## The communication cycle + +Custom components communicate through a cycle: + +1. **Python → JavaScript**: Python sends data to your component via the `data` parameter. +2. **User interaction**: The user interacts with your component in the browser. +3. **JavaScript → Python**: Your component sends the result back via `setStateValue()` or `setTriggerValue()`. +4. **Python updates**: All related callback functions are executed and the component's result is updated in a script rerun. + +For native Streamlit widgets, you can assign a key and set a widget's state through Session State. However, custom components don't automatically pass information from Session State to their associated component. To programmatically update a component from Python, you need to pass new data to the component's `data` parameter. If you want to set your component's state from Session State, you must pass the component's Session State values to the component's `data` parameter. + +## Creating a feedback loop + +Here's a text input component that demonstrates this pattern. This is the [text input](/develop/concepts/custom-components/components-v2/examples/text-input) component shown in the quickstart guide. + + + +### The JavaScript side + +Your component's JavaScript function must read `data` to initialize and update its state: + +```javascript +export default function (component) { + const { setStateValue, parentElement, data } = component; + + const label = parentElement.querySelector("label"); + label.innerText = data.label; + + const input = parentElement.querySelector("input"); + // Sync input value with data from Python + if (input.value !== data.value) { + input.value = data.value ?? ""; + } + + input.onkeydown = (e) => { + if (e.key === "Enter") { + setStateValue("value", e.target.value); + } + }; + + input.onblur = (e) => { + setStateValue("value", e.target.value); + }; +} +``` + +The conditional expression (`if (input.value !== data.value)`) updates the input field when Python sends new data. Because the Python code (in the next section) sets the state value using the `default` parameter, the component doesn't need to use `setStateValue()` here. + +### The Python side + +In Python, you can create a wrapper function that reads from Session State and passes updated data: + +```python +def textbox_component_wrapper( + label, *, default="", key=None, on_change=lambda: None +): + # Read current state from Session State + if key is not None: + component_state = st.session_state.get(key, {}) + value = component_state.get("value", default) + else: + value = default + + # Pass current value back to component + data = {"label": label, "value": value} + result = textbox_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result +``` + +To create a clean mounting command, the wrapper lets you declare the component's label, initial value, key, and callback function. Within the +wrapper, when a key is provided, use the `get()` method on `st.session_state` to read the current component state. This prevents an error on the first script run, before the component is mounted. + +The `get()` method is used twice. First, get the component state from its key. Component states are dictionaries of state and trigger values. In this case, the component has a single state named `"value"`. Then, from the component state, get the value of the the `"value"` state. If the `"value"` state isn't defined or no key is provided, use the provided default value. + +Finally, within the wrapper, call the raw mounting command, passing in the current data. You can directly pass through the `key` and `on_change` values. However, `data` and `default` are constructed from the previous logic that uses the existing component state. + +### Programmatic updates + +With this pattern, you can update the component from Python by modifying Session State: + +```python +if st.button("Set greeting"): + st.session_state["my_textbox"]["value"] = "Hello World" + +if st.button("Clear"): + st.session_state["my_textbox"]["value"] = "" + +result = textbox_component_wrapper("Enter text", key="my_textbox") +``` + +When you click a button, it modifies Session State. On the rerun, the wrapper reads the new value from Session State and passes it to the component via `data`. The JavaScript sees the updated `data.value` and updates the input field. + +## Complete example + +See the [Text input component example](/develop/concepts/custom-components/components-v2/examples/text-input) for the full working code. + +## Key takeaways + +1. **`data` is one-way**: Python sends data to JavaScript, but changes in JavaScript don't automatically update Python. +2. **State values bridge the gap**: Use `setStateValue()` to send user interactions back to Python. +3. **Session State enables control**: Store component state with a `key`, then read from Session State to update `data`. +4. **Sync carefully**: Check if the value has changed before updating DOM elements to avoid interfering with user input. diff --git a/content/develop/concepts/custom-components/components-v2/examples/_index.md b/content/develop/concepts/custom-components/components-v2/examples/_index.md new file mode 100644 index 000000000..6f62b6a92 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/_index.md @@ -0,0 +1,386 @@ +--- +title: Quickstart examples +slug: /develop/concepts/custom-components/components-v2/examples +description: Get started quickly with Custom Components v2 through practical examples showing interactive buttons, data exchange, and complete component implementations. +keywords: custom components v2, quickstart, examples, interactive components, data exchange, component examples, getting started +--- + +# Quickstart examples + +Get started with custom components v2 through these practical examples. Each example introduces a new concept to progressively build your understanding. To highlight each concept, the code on this page shows either a portion of the component example or a simplified version of it. Follow the links below each example to see the complete code for that example, including explanations. + +## Two-step component process + +Creating and using a custom component involves two distinct steps: + +1. **Registration**: Define your component's HTML, CSS, and JavaScript with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). +2. **Mounting**: Mount a specific instance of your component to your app's frontend using the [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) created during registration. + +For detailed explanations, see [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount). + +## Hello world + +This is a minimal static component that displays "Hello, World!" using the app's primary theme color. This component introduces the following concepts: + +- Component registration with HTML and CSS using `st.components.v2.component()` +- Theme integration using CSS custom properties +- Mounting a component by calling the `ComponentRenderer` + + + +```python filename="streamlit_app.py" +import streamlit as st + +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }", +) + +hello_component() +``` + + + +## Rich data + +This is a component that receives various data types from Python. This component introduces the following concepts: + +- Passing data from Python via the `data` parameter and accessing it in JavaScript +- Automatic dataframe and JSON serialization +- Passing an image as a Base64-encoded string +- Using a placeholder in the component's HTML and dynamically updating it with received data + + + +```python +data_component = st.components.v2.component( + "data_display", + html="""
Loading data...
""", + js=""" + export default function({ data, parentElement }) { + const container = parentElement.querySelector("#data-container"); + const df = data.df; + const userInfo = data.user_info; + const imgBase64 = data.image_base64; + container.innerHTML = ` +

Dataframe: ${df}

+

User Info: ${userInfo.name}

+ + `; + } + """, +) + +data_component( + data={ + "df": df, # Arrow-serializable + "user_info": {"name": "Alice"}, # JSON-serializable + "image_base64": img_base64 # Base64 string + } +) +``` + + + +## Simple button + +This is an interactive button that sends events to Python. This component introduces the following concepts: + +- Component registration with HTML, CSS, and JavaScript +- One-time trigger values sent from JavaScript with `setTriggerValue()` +- Callback functions using the `on__change` naming pattern +- Accessing trigger values from the component's return object + + + +```python filename="streamlit_app.py" +import streamlit as st + +def handle_button_click(): + st.session_state.click_count += 1 + +st.session_state.setdefault("click_count", 0) + +button_component = st.components.v2.component( + "simple_button", + html="""""", + css="""button { background-color: var(--st-primary-color); }""", + js=""" + export default function(component) { + const { setTriggerValue, parentElement } = component; + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); + }; + } + """, +) + +result = button_component(on_action_change=handle_button_click) + +st.write(result.action) +st.write(f"Total clicks: {st.session_state.click_count}") +``` + + + +## Simple checkbox + +This is a simple checkbox that reports a stateful value to Python. This component introduces the following concepts: + +- Persistent state values sent from JavaScript with `setStateValue()` +- Callback functions with the `on__change` naming pattern +- Initializing a stateful component with the `data` and `default` parameters +- Using font from the app's theme +- Accessing state values from the component's return object + + + +```python filename="streamlit_app.py" +import streamlit as st + +checkbox_component = st.components.v2.component( + "simple_checkbox", + html=""" + + """, + css=""" + .checkbox-container { + gap: 0.5rem; + font-family: var(--st-font); + color: var(--st-text-color); + } + """, + js=""" + export default function({ parentElement, data, setStateValue }) { + const checkbox = parentElement.querySelector("#checkbox"); + checkbox.checked = data?.checked ?? false; + + checkbox.onchange = () => { + setStateValue("checked", checkbox.checked); + }; + } + """, +) + +initial_state = True + +result = checkbox_component( + data={"checked": initial_state}, + default={"checked": initial_state}, + on_checked_change=lambda: None, +) + +st.write(f"Current state: {'Enabled' if result.checked else 'Disabled'}") +``` + + + +## Interactive counter + +This is a counter with increment, decrement, and reset functionality. This component introduces the following concepts: + +- Combining state and trigger values in one component +- Multiple event handlers + + + +```markup +
+

Count: 0

+
+ + + +
+
+``` + +```javascript +export default function ({ parentElement, setStateValue, setTriggerValue }) { + const incrementBtn = parentElement.querySelector("#increment"); + const decrementBtn = parentElement.querySelector("#decrement"); + const resetBtn = parentElement.querySelector("#reset"); + let count = 0; + + decrementBtn.onclick = () => { + count--; + setStateValue("count", count); // Persistent state + }; + + incrementBtn.onclick = () => { + count++; + setStateValue("count", count); // Persistent state + }; + + resetBtn.onclick = () => { + count = 0; + setTriggerValue("reset", true); // One-time event + setStateValue("count", 0); + }; +} +``` + + + +## Text input + +This is a text input component that demonstrates full bidirectional communication, including programmatic updates from Python. This component introduces the following concepts: + +- Mounting a component with a key and reading component state from Session State +- Wrapping a component's raw mounting command to create a user-friendly mounting command +- Programmatic updates from Python via the `data` parameter +- Syncing frontend state without interrupting user input + + + +```python +def textbox_component_wrapper( + label, *, default="", key=None, on_change=lambda: None +): + # Read current state from Session State + component_state = st.session_state.get(key, {}) + value = component_state.get("value", default) + + # Pass current value to component + data = {"label": label, "value": value} + result = textbox_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result +``` + +```javascript +const input = parentElement.querySelector("input"); +// Sync input value with data from Python +if (input.value !== data.value) { + input.value = data.value ?? ""; +} +``` + + + +## Danger button + +This is a hold-to-confirm button with frontend validation and visual feedback. This component introduces the following concepts: + +- Frontend validation before sending data to Python +- Timed interactions with `requestAnimationFrame()` +- Visual feedback with CSS animations and transitions +- Rate limiting with cooldown periods +- Touch events for mobile support +- Layout control using the `width` parameter +- Cleanup functions for event listeners + + + +```javascript +function startHold() { + startTime = Date.now(); + animationFrame = requestAnimationFrame(updateProgress); +} + +function updateProgress() { + const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); + + if (progressPercent >= 1) { + setTriggerValue("confirmed", true); // Only after 2 seconds + } else { + animationFrame = requestAnimationFrame(updateProgress); + } +} +``` + +```python +result = danger_button( + on_confirmed_change=on_delete_confirmed, + width="content" # Layout control +) +``` + + + +## Radial menu + +This is a circular selection menu demonstrating state values for persistent selections. This component introduces the following concepts: + +- CSS custom properties for dynamic positioning (`--i`, `--total`) +- A fixed-position backdrop for click-outside behavior +- Complex animations with CSS transitions + + + +```python +result = radial_menu( + data={"options": options, "selection": "burger"}, + default={"selection": "burger"}, # Avoids initial rerun + on_selection_change=lambda: None, +) +``` + +```javascript +// Dynamic element creation +Object.entries(options).forEach(([value, icon], index) => { + const button = document.createElement("button"); + button.style.setProperty("--i", index); + button.style.setProperty("--total", Object.keys(options).length); + // ... +}); +``` + + + +## What's next? + +Now that you've seen these examples: + +- Learn the fundamentals in [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount). +- Understand [State versus trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) for advanced interactions. +- Explore [Theming and styling](/develop/concepts/custom-components/components-v2/theming) to make beautiful components. +- Build complex projects with [Package-based components](/develop/concepts/custom-components/components-v2/package-based). diff --git a/content/develop/concepts/custom-components/components-v2/examples/danger-button.md b/content/develop/concepts/custom-components/components-v2/examples/danger-button.md new file mode 100644 index 000000000..28e83377c --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/danger-button.md @@ -0,0 +1,697 @@ +--- +title: "Component example: Danger Button" +slug: /develop/concepts/custom-components/components-v2/examples/danger-button +description: A hold-to-confirm button with frontend validation, visual feedback, and rate limiting. +keywords: custom components v2, example, danger button, hold to confirm, frontend validation, animations, touch events +--- + +# Component example: Danger Button + +This is a button that requires the user to hold for two seconds to confirm a dangerous action. It demonstrates frontend validation, visual feedback, and rate limiting. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Frontend validation before sending data to Python +- Timed interactions with `requestAnimationFrame()` +- Visual feedback with CSS animations and transitions +- Rate limiting with cooldown periods +- Touch events for mobile support +- Layout control using the `width` parameter +- Cleanup functions for event listeners + +## Complete code + +For easy copying, expand the complete code below. For easier reading, the HTML, CSS, and JavaScript are shown separately. + + + +```python filename="streamlit_app.py" +import streamlit as st + +danger_button = st.components.v2.component( + name="hold_to_confirm", + html=""" + + """, + css=""" + .hold-button { + position: relative; + width: 7.5rem; + height: 7.5rem; + padding: 0 2rem; + border-radius: 50%; + border: 1px solid var(--st-primary-color); + background: var(--st-secondary-background-color); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + .hold-button:hover { + transform: scale(1.05); + border-color: var(--st-red-color); + } + + .hold-button:active:not(:disabled) { + transform: scale(0.98); + } + + .hold-button:disabled { + cursor: not-allowed; + opacity: 0.9; + } + + .hold-button.holding { + animation: pulse 0.5s ease-in-out infinite; + border-color: var(--st-red-color); + } + + .hold-button.triggered { + animation: success-burst 0.6s ease-out forwards; + } + + @keyframes pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 var(--st-red-color); + } + 50% { + box-shadow: 0 0 0 15px transparent; + } + } + + @keyframes success-burst { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.15); + background: var(--st-red-background-color); + } + 100% { + transform: scale(1); + } + } + + .progress-ring { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform: rotate(-90deg); + } + + .ring-bg { + fill: none; + stroke: var(--st-border-color); + stroke-width: 4; + } + + .ring-progress { + fill: none; + stroke: var(--st-red-color); + stroke-width: 4; + stroke-linecap: round; + stroke-dasharray: 283; + stroke-dashoffset: 283; + transition: stroke-dashoffset 0.1s linear; + filter: drop-shadow(0 0 0.5rem var(--st-red-color)); + } + + .button-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + font-family: var(--st-font); + } + + .icon { + font-size: 2rem; + transition: transform 0.3s ease; + } + + .hold-button:hover .icon { + transform: scale(1.1); + } + + .hold-button.holding .icon { + animation: shake 0.15s ease-in-out infinite; + } + + @keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-2px) rotate(-5deg); + } + 75% { + transform: translateX(2px) rotate(5deg); + } + } + + .label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--st-text-color); + opacity: 0.6; + transition: all 0.3s ease; + } + + .hold-button.holding .label { + color: var(--st-red-color); + opacity: 1; + } + + .hold-button.triggered .icon, + .hold-button.triggered .label { + color: var(--st-primary-color); + opacity: 1; + } + """, + js=""" + const HOLD_DURATION = 2000; // 2 seconds + const COOLDOWN_DURATION = 1500; // cooldown after trigger + const CIRCUMFERENCE = 2 * Math.PI * 45; // circle circumference + + export default function ({ parentElement, setTriggerValue, data }) { + const button = parentElement.querySelector("#danger-btn"); + const progress = parentElement.querySelector("#ring-progress"); + const icon = parentElement.querySelector("#icon"); + const label = parentElement.querySelector("#label"); + + let startTime = null; + let animationFrame = null; + let isDisabled = false; // Prevent interaction during cooldown + + function updateProgress() { + if (!startTime) return; + + const elapsed = Date.now() - startTime; + const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); + const offset = CIRCUMFERENCE * (1 - progressPercent); + + progress.style.strokeDashoffset = offset; + + if (progressPercent >= 1) { + // Triggered! + triggerAction(); + } else { + animationFrame = requestAnimationFrame(updateProgress); + } + } + + function startHold() { + if (isDisabled) return; // Ignore if in cooldown + + startTime = Date.now(); + button.classList.add("holding"); + label.textContent = data?.continue ?? "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } + + function cancelHold() { + if (isDisabled) return; // Ignore if in cooldown + + startTime = null; + button.classList.remove("holding"); + label.textContent = data?.start ?? "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; + + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + } + + function triggerAction() { + cancelAnimationFrame(animationFrame); + animationFrame = null; + startTime = null; + isDisabled = true; // Disable during cooldown + + button.classList.remove("holding"); + button.classList.add("triggered"); + button.disabled = true; + + icon.textContent = "✓"; + label.textContent = data?.completed ?? "Deleted!"; + progress.style.strokeDashoffset = 0; + + // Send trigger to Python + setTriggerValue("confirmed", true); + + // Reset after cooldown + setTimeout(() => { + button.classList.remove("triggered"); + button.disabled = false; + isDisabled = false; + icon.textContent = data?.icon ?? "🗑️"; + label.textContent = data?.start ?? "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; + }, COOLDOWN_DURATION); + } + + function handleTouchStart(e) { + e.preventDefault(); + startHold(); + } + + // Mouse events + button.addEventListener("mousedown", startHold); + button.addEventListener("mouseup", cancelHold); + button.addEventListener("mouseleave", cancelHold); + button.addEventListener("contextmenu", cancelHold); // Ctrl+Click on Mac + + // Touch events for mobile + button.addEventListener("touchstart", handleTouchStart); + button.addEventListener("touchend", cancelHold); + button.addEventListener("touchcancel", cancelHold); + + return () => { + if (animationFrame) cancelAnimationFrame(animationFrame); + + // Remove mouse event listeners + button.removeEventListener("mousedown", startHold); + button.removeEventListener("mouseup", cancelHold); + button.removeEventListener("mouseleave", cancelHold); + button.removeEventListener("contextmenu", cancelHold); + + // Remove touch event listeners + button.removeEventListener("touchstart", handleTouchStart); + button.removeEventListener("touchend", cancelHold); + button.removeEventListener("touchcancel", cancelHold); + }; + } + """, +) + +st.title("Hold-to-Confirm Button") +st.caption("A dangerous action that requires intentional confirmation") + +# Track deletion events +if "deleted_items" not in st.session_state: + st.session_state.deleted_items = [] + + +# Callback when deletion is confirmed +def on_delete_confirmed(): + st.session_state.deleted_items.append( + f"Deleted item #{len(st.session_state.deleted_items) + 1}" + ) + st.toast("Item permanently deleted!", icon="🗑️") + + +# Render the component +with st.container(horizontal_alignment="center"): + result = danger_button( + key="danger_btn", + on_confirmed_change=on_delete_confirmed, + width="content" + ) + +# Show deletion history +if st.session_state.deleted_items: + st.divider() + st.subheader("Deletion Log") + for item in reversed(st.session_state.deleted_items[-3:]): + st.write(f"• {item}") +``` + + + +```markup + +``` + + + +```css +.hold-button { + position: relative; + width: 7.5rem; + height: 7.5rem; + padding: 0 2rem; + border-radius: 50%; + border: 1px solid var(--st-primary-color); + background: var(--st-secondary-background-color); + cursor: pointer; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.hold-button:hover { + transform: scale(1.05); + border-color: var(--st-red-color); +} + +.hold-button:active:not(:disabled) { + transform: scale(0.98); +} + +.hold-button:disabled { + cursor: not-allowed; + opacity: 0.9; +} + +.hold-button.holding { + animation: pulse 0.5s ease-in-out infinite; + border-color: var(--st-red-color); +} + +.hold-button.triggered { + animation: success-burst 0.6s ease-out forwards; +} + +@keyframes pulse { + 0%, + 100% { + box-shadow: 0 0 0 0 var(--st-red-color); + } + 50% { + box-shadow: 0 0 0 15px transparent; + } +} + +@keyframes success-burst { + 0% { + transform: scale(1); + } + 50% { + transform: scale(1.15); + background: var(--st-red-background-color); + } + 100% { + transform: scale(1); + } +} + +.progress-ring { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + transform: rotate(-90deg); +} + +.ring-bg { + fill: none; + stroke: var(--st-border-color); + stroke-width: 4; +} + +.ring-progress { + fill: none; + stroke: var(--st-red-color); + stroke-width: 4; + stroke-linecap: round; + stroke-dasharray: 283; + stroke-dashoffset: 283; + transition: stroke-dashoffset 0.1s linear; + filter: drop-shadow(0 0 0.5rem var(--st-red-color)); +} + +.button-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + font-family: var(--st-font); +} + +.icon { + font-size: 2rem; + transition: transform 0.3s ease; +} + +.hold-button:hover .icon { + transform: scale(1.1); +} + +.hold-button.holding .icon { + animation: shake 0.15s ease-in-out infinite; +} + +@keyframes shake { + 0%, + 100% { + transform: translateX(0); + } + 25% { + transform: translateX(-2px) rotate(-5deg); + } + 75% { + transform: translateX(2px) rotate(5deg); + } +} + +.label { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--st-text-color); + opacity: 0.6; + transition: all 0.3s ease; +} + +.hold-button.holding .label { + color: var(--st-red-color); + opacity: 1; +} + +.hold-button.triggered .icon, +.hold-button.triggered .label { + color: var(--st-primary-color); + opacity: 1; +} +``` + + + + + +```javascript +const HOLD_DURATION = 2000; // 2 seconds +const COOLDOWN_DURATION = 1500; // cooldown after trigger +const CIRCUMFERENCE = 2 * Math.PI * 45; // circle circumference + +export default function ({ parentElement, setTriggerValue, data }) { + const button = parentElement.querySelector("#danger-btn"); + const progress = parentElement.querySelector("#ring-progress"); + const icon = parentElement.querySelector("#icon"); + const label = parentElement.querySelector("#label"); + + let startTime = null; + let animationFrame = null; + let isDisabled = false; // Prevent interaction during cooldown + + function updateProgress() { + if (!startTime) return; + + const elapsed = Date.now() - startTime; + const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); + const offset = CIRCUMFERENCE * (1 - progressPercent); + + progress.style.strokeDashoffset = offset; + + if (progressPercent >= 1) { + triggerAction(); + } else { + animationFrame = requestAnimationFrame(updateProgress); + } + } + + function startHold() { + if (isDisabled) return; + + startTime = Date.now(); + button.classList.add("holding"); + label.textContent = data?.continue ?? "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } + + function cancelHold() { + if (isDisabled) return; + + startTime = null; + button.classList.remove("holding"); + label.textContent = data?.start ?? "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; + + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + } + + function triggerAction() { + cancelAnimationFrame(animationFrame); + animationFrame = null; + startTime = null; + isDisabled = true; + + button.classList.remove("holding"); + button.classList.add("triggered"); + button.disabled = true; + + icon.textContent = "✓"; + label.textContent = data?.completed ?? "Deleted!"; + progress.style.strokeDashoffset = 0; + + setTriggerValue("confirmed", true); + + setTimeout(() => { + button.classList.remove("triggered"); + button.disabled = false; + isDisabled = false; + icon.textContent = data?.icon ?? "🗑️"; + label.textContent = data?.start ?? "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; + }, COOLDOWN_DURATION); + } + + function handleTouchStart(e) { + e.preventDefault(); + startHold(); + } + + // Mouse events + button.addEventListener("mousedown", startHold); + button.addEventListener("mouseup", cancelHold); + button.addEventListener("mouseleave", cancelHold); + button.addEventListener("contextmenu", cancelHold); + + // Touch events for mobile + button.addEventListener("touchstart", handleTouchStart); + button.addEventListener("touchend", cancelHold); + button.addEventListener("touchcancel", cancelHold); + + return () => { + if (animationFrame) cancelAnimationFrame(animationFrame); + + button.removeEventListener("mousedown", startHold); + button.removeEventListener("mouseup", cancelHold); + button.removeEventListener("mouseleave", cancelHold); + button.removeEventListener("contextmenu", cancelHold); + + button.removeEventListener("touchstart", handleTouchStart); + button.removeEventListener("touchend", cancelHold); + button.removeEventListener("touchcancel", cancelHold); + }; +} +``` + + + +```python filename="streamlit_app.py" +import streamlit as st + +danger_button = st.components.v2.component( + name="hold_to_confirm", + html="...", + css="...", + js="...", +) + +st.title("Hold-to-Confirm Button") +st.caption("A dangerous action that requires intentional confirmation") + +if "deleted_items" not in st.session_state: + st.session_state.deleted_items = [] + + +def on_delete_confirmed(): + st.session_state.deleted_items.append( + f"Deleted item #{len(st.session_state.deleted_items) + 1}" + ) + st.toast("Item permanently deleted!", icon="🗑️") + + +with st.container(horizontal_alignment="center"): + result = danger_button( + key="danger_btn", + on_confirmed_change=on_delete_confirmed, + width="content" + ) + +if st.session_state.deleted_items: + st.divider() + st.subheader("Deletion Log") + for item in reversed(st.session_state.deleted_items[-3:]): + st.write(f"• {item}") +``` + +## How it works + +### Frontend validation + +The trigger only fires after the user holds for the full 2 seconds. If they release early, `cancelHold()` resets the progress: + +```javascript +if (progressPercent >= 1) { + triggerAction(); +} else { + animationFrame = requestAnimationFrame(updateProgress); +} +``` + +### Rate limiting + +After triggering, the button enters a cooldown period where interactions are ignored: + +```javascript +isDisabled = true; +setTimeout(() => { + isDisabled = false; + // ... reset visual state +}, COOLDOWN_DURATION); +``` + +### Touch support + +The component handles both mouse and touch events for mobile compatibility: + +```javascript +button.addEventListener("touchstart", handleTouchStart); +button.addEventListener("touchend", cancelHold); +button.addEventListener("touchcancel", cancelHold); +``` + +### Customizable labels + +Labels are customizable via the `data` parameter with fallback defaults: + +```javascript +label.textContent = data?.start ?? "Hold to Delete"; +label.textContent = data?.continue ?? "Keep holding..."; +label.textContent = data?.completed ?? "Deleted!"; +``` diff --git a/content/develop/concepts/custom-components/components-v2/examples/hello-world.md b/content/develop/concepts/custom-components/components-v2/examples/hello-world.md new file mode 100644 index 000000000..b00c4e6ee --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/hello-world.md @@ -0,0 +1,52 @@ +--- +title: "Component example: Hello world" +slug: /develop/concepts/custom-components/components-v2/examples/hello-world +description: A simple static component that displays themed text using Streamlit's CSS custom properties. +keywords: custom components v2, example, hello world, static component, theming +--- + +# Component example: Hello world + +This is a minimal static component that displays "Hello, World!" using the app's primary theme color. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Component registration with HTML and CSS using [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component) +- Theme integration using CSS custom properties +- Mounting a component by calling the [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) + +## Complete code + +```python filename="streamlit_app.py" +import streamlit as st + +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }", +) + +hello_component() +``` + +## How it works + +### Registration + +The component is registered with the following parameters: + +- `name`: `"hello_world"` is a unique identifier that Streamlit uses internally to retrieve the component's HTML and CSS code when an instance of the component is mounted. +- `html`: `"

Hello, World!

"` is the markup that Streamlit renders in the component's DOM. +- `css`: `"h2 { color: var(--st-primary-color); }"` uses a CSS custom property to apply the app's primary color to the component's heading element. + +### Theming + +Most theme configuration options can be converted from camel case to dash-case and used as CSS custom properties. For example, `theme.primaryColor` becomes `--st-primary-color`. In this example, `var(--st-primary-color)` is used to apply the app's primary color to the component's heading. If an app has a light and dark theme configured, the CSS custom property will reflect the value of the current theme. For more theme variables, see [Theming and styling](/develop/concepts/custom-components/components-v2/theming). + +### Mounting + +The registration command returns a [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) that can be called to mount an instance of the component. In this example, the component is mounted by calling `hello_component()`. Because this is a static component with no interactivity, no additional parameters are needed to mount the component. diff --git a/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md b/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md new file mode 100644 index 000000000..9ef4f2891 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md @@ -0,0 +1,255 @@ +--- +title: "Component example: Interactive counter" +slug: /develop/concepts/custom-components/components-v2/examples/interactive-counter +description: A counter component demonstrating state values, trigger values, multiple event handlers, and cleanup functions. +keywords: custom components v2, example, counter, state values, trigger values, cleanup function, event handlers +--- + +# Component example: Interactive counter + +This is a counter component that can be incremented, decremented, and reset. It demonstrates combining state values (persistent count) with trigger values (reset event). + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Combining state and trigger values in one component +- Multiple event handlers + +## Complete code + +For easy copying, expand the complete code below. For easier reading, the HTML, CSS, and JavaScript are shown separately. + + + +```python filename="streamlit_app.py" +import streamlit as st + +counter_component = st.components.v2.component( + "interactive_counter", + html=""" +
+

Count: 0

+
+ + + +
+
+ """, + css=""" + .counter { + padding: 2rem; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + text-align: center; + } + + .buttons { + margin-top: 1rem; + } + + button { + margin: 0 0.5rem; + padding: 0.5rem 1rem; + background: var(--st-primary-color); + color: white; + border: none; + border-radius: var(--st-button-radius); + cursor: pointer; + } + + button:hover { + opacity: 0.8; + } + + #reset { + background: var(--st-red-color); + } + """, + js=""" + export default function ({ + parentElement, + setStateValue, + setTriggerValue, + data, + }) { + let count = data?.initialCount || 0; + const display = parentElement.querySelector("#display"); + const incrementBtn = parentElement.querySelector("#increment"); + const decrementBtn = parentElement.querySelector("#decrement"); + const resetBtn = parentElement.querySelector("#reset"); + + const updateDisplay = () => { + display.textContent = count; + setStateValue("count", count); // Persistent state + }; + + incrementBtn.onclick = () => { + count++; + updateDisplay(); + }; + + decrementBtn.onclick = () => { + count--; + updateDisplay(); + }; + + resetBtn.onclick = () => { + count = 0; + updateDisplay(); + setTriggerValue("reset", true); // One-time trigger + }; + + // Initialize + updateDisplay(); + } + """, +) + +result = counter_component( + default={"count": 0}, + data={"initialCount": 0}, + on_count_change=lambda: None, # Track count state + on_reset_change=lambda: None, # Handle reset events +) + +# Display current state +st.write(f"Current count: {result.count}") + +# Show when reset was triggered (only for one rerun) +if result.reset: + st.toast("Counter was reset!") +``` + +
+ +```markup +
+

Count: 0

+
+ + + +
+
+``` + +```css +.counter { + padding: 2rem; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + text-align: center; +} + +.buttons { + margin-top: 1rem; +} + +button { + margin: 0 0.5rem; + padding: 0.5rem 1rem; + background: var(--st-primary-color); + color: white; + border: none; + border-radius: var(--st-button-radius); + cursor: pointer; +} + +button:hover { + opacity: 0.8; +} + +#reset { + background: var(--st-red-color); +} +``` + +```javascript +export default function ({ + parentElement, + setStateValue, + setTriggerValue, + data, +}) { + let count = data?.initialCount || 0; + const display = parentElement.querySelector("#display"); + const incrementBtn = parentElement.querySelector("#increment"); + const decrementBtn = parentElement.querySelector("#decrement"); + const resetBtn = parentElement.querySelector("#reset"); + + const updateDisplay = () => { + display.textContent = count; + setStateValue("count", count); // Persistent state + }; + + incrementBtn.onclick = () => { + count++; + updateDisplay(); + }; + + decrementBtn.onclick = () => { + count--; + updateDisplay(); + }; + + resetBtn.onclick = () => { + count = 0; + updateDisplay(); + setTriggerValue("reset", true); // One-time trigger + }; + + // Initialize + updateDisplay(); +} +``` + +```python filename="streamlit_app.py" +import streamlit as st + +counter_component = st.components.v2.component( + "interactive_counter", + html="...", + css="...", + js="...", +) + +result = counter_component( + default={"count": 0}, + data={"initialCount": 0}, + on_count_change=lambda: None, + on_reset_change=lambda: None, +) + +st.write(f"Current count: {result.count}") + +if result.reset: + st.toast("Counter was reset!") +``` + +## How it works + +### State and trigger values + +You can have multiple state and trigger values for a single component, and this component uses both. The `count` state value is persistent across reruns and the `reset` trigger value is transient, returning `True` for one rerun when the reset button is clicked. + +The increment, decrement, and reset buttons have event listeners that update the `count` state value and report the change to Python with `setStateValue()`. Additionally, the reset button sets the `reset` trigger value to `True` with `setTriggerValue()`, which is only available for one rerun. This means that when the reset button is clicked, both the state and trigger values are updated, but this will only trigger a single rerun of the script. + +### Component initialization + +The `data.initialCount` value sets the initial value of the component on the frontend. The `default` parameter sets the initial value of the component in Python. Using optional chaining (`data?.initialCount`) handles cases where data might be undefined. In this example, the component will fallback to an initial count of `0` if no initial count is provided in `data`. Therefore, when the initial count is `0`, you can omit the `data` parameter. + +### The `on_count_change` and `on_reset_change` callbacks + +The callbacks ensure that the `count` and `reset` attributes are available in the component's result object before they are set from the frontend. This is important because the component's result object is used to access the component's state and trigger values in Python. Without the callbacks, your Python code must check for the presence of the attributes before accessing them. + +In this example, the callbacks are set to `lambda: None`, which is an empty callback function. + +### Multifile component structure + +When your component is complex enough to warrant multiple files, it's recommended to use package-based development, which requires defining `pyproject.toml` files and understanding the basics of packaging Python projects. For simplicity, this example uses inline development and passes the HTML, CSS, and JavaScript as strings. This is because you can't use file references with inline components. After understanding the basics of creating a custom component with inline development, you can explore [package-based development](/develop/concepts/custom-components/components-v2/package-based). diff --git a/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md b/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md new file mode 100644 index 000000000..c2a352926 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md @@ -0,0 +1,649 @@ +--- +title: "Component example: Radial Menu" +slug: /develop/concepts/custom-components/components-v2/examples/radial-menu +description: A radial selection menu demonstrating state values for persistent selections. +keywords: custom components v2, example, radial menu, state values, setStateValue, dynamic elements +--- + +# Component example: Radial Menu + +This is a circular menu component that allows users to select from a list of options. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- CSS custom properties for dynamic positioning (`--i`, `--total`) +- A fixed-position backdrop for click-outside behavior +- Complex animations with CSS transitions + +## Complete code + +For easy copying, expand the complete code below. For easier reading, the HTML, CSS, and JavaScript are shown separately. + + + +```python filename="streamlit_app.py" +import streamlit as st + +radial_menu = st.components.v2.component( + name="radial_menu", + html=""" +
+ + + + + +
+ """, + css=""" + .radial-menu { + position: relative; + display: inline-block; + font-family: var(--st-font); + } + + /* The circular selector button and menu items*/ + .menu-selector, + .menu-item { + width: 3.25rem; + height: 3.25rem; + border-radius: 50%; + border: 2px solid var(--st-border-color); + cursor: pointer; + background: var(--st-secondary-background-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-size: 1.5rem; + } + + .menu-selector:hover { + transform: scale(1.05); + border-color: var(--st-primary-color); + } + + /* Overlay container */ + .menu-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; + pointer-events: none; + } + + /* The ring of menu items */ + .menu-ring { + position: relative; + width: 13rem; + height: 13rem; + transform: scale(0); + opacity: 0; + transition: + transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), + opacity 0.2s ease; + } + + .menu-ring.open { + transform: scale(1); + opacity: 1; + pointer-events: auto; + } + + /* Menu items arranged in a circle */ + .menu-item { + --angle: calc(var(--i) * (360deg / var(--total)) - 90deg); + --radius: 4rem; + + background: var(--st-background-color); + position: absolute; + top: 50%; + left: 50%; + margin: -1.6125rem; + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))); + } + + .menu-item:hover { + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))) scale(1.15); + border-color: var(--st-primary-color); + background: var(--st-secondary-background-color); + } + + .menu-item.selected { + border-color: var(--st-primary-color); + background: var(--st-secondary-background-color); + } + + /* Backdrop when menu is open (must be outside .menu-overlay to avoid + its transform creating a containing block for position: fixed) */ + .menu-backdrop { + display: none; + position: fixed; + inset: 0; + background: var(--st-background-color); + opacity: 0; + transition: opacity 0.3s ease; + z-index: 99; + } + + .menu-backdrop.open { + display: block; + opacity: 0.7; + } + + /* Center decoration */ + .menu-ring::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 2rem; + height: 2rem; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--st-secondary-background-color); + border: 2px dashed var(--st-border-color); + opacity: 0.6; + box-sizing: border-box; + } + """, + js=""" + export default function ({ parentElement, data, setStateValue }) { + const selector = parentElement.querySelector("#selector"); + const selectorIcon = parentElement.querySelector("#selector-icon"); + const overlay = parentElement.querySelector("#overlay"); + const backdrop = parentElement.querySelector("#backdrop"); + const ring = parentElement.querySelector("#ring"); + + let isOpen = false; + const options = data?.options || {}; + let currentSelection = data?.selection || Object.keys(options)[0]; + const optionCount = Object.keys(options).length; + + // Set total item count for CSS angle calculation + ring.style.setProperty("--total", optionCount); + + // Create the menu items from options + Object.entries(options).forEach(([value, icon], index) => { + const button = document.createElement("button"); + button.className = "menu-item"; + button.dataset.value = value; + button.style.setProperty("--i", index); + button.textContent = icon; + + button.onclick = () => { + currentSelection = value; + updateDisplay(); + toggleMenu(); + setStateValue("selection", currentSelection); + }; + + ring.appendChild(button); + }); + + // Update the selector icon and highlight selected item + function updateDisplay() { + selectorIcon.textContent = options[currentSelection] || "?"; + + ring.querySelectorAll(".menu-item").forEach((item) => { + item.classList.toggle( + "selected", + item.dataset.value === currentSelection, + ); + }); + } + + // Calculate and apply viewport-safe position for the menu + function updatePosition() { + const selectorRect = selector.getBoundingClientRect(); + const menuRadius = ring.offsetWidth / 2; + const toolbarHeight = 60; // Streamlit toolbar height + + // Center of selector in viewport + const centerX = selectorRect.left + selectorRect.width / 2; + const centerY = selectorRect.top + selectorRect.height / 2; + + // Calculate overflow on each side (account for toolbar at top) + const overflowLeft = menuRadius - centerX; + const overflowRight = centerX + menuRadius - window.innerWidth; + const overflowTop = menuRadius - (centerY - toolbarHeight); + const overflowBottom = centerY + menuRadius - window.innerHeight; + + // Apply offset to keep menu in viewport + const offsetX = Math.max(0, overflowLeft) - Math.max(0, overflowRight); + const offsetY = Math.max(0, overflowTop) - Math.max(0, overflowBottom); + + overlay.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`; + } + + // Debounced resize handler for performance + let resizeTimeout; + function handleResize() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(updatePosition, 16); // ~60fps + } + + // Toggle menu open/closed + function toggleMenu() { + isOpen = !isOpen; + backdrop.classList.toggle("open", isOpen); + overlay.classList.toggle("open", isOpen); + ring.classList.toggle("open", isOpen); + + if (isOpen) { + updatePosition(); + window.addEventListener("resize", handleResize); + } else { + window.removeEventListener("resize", handleResize); + clearTimeout(resizeTimeout); + // Reset position after close animation completes (400ms matches CSS transition) + setTimeout(() => { + if (!isOpen) overlay.style.transform = ""; + }, 400); + } + } + + // Initialize display + updateDisplay(); + + // Attach click handlers + selector.onclick = toggleMenu; + backdrop.onclick = () => toggleMenu(); + + // Cleanup function + return () => { + clearTimeout(resizeTimeout); + window.removeEventListener("resize", handleResize); + }; + } + """, +) + +st.header("Radial Menu Component") + +st.write("Click the button to open the menu. Select your favorite food!") + +options = { + "pizza": "🍕", + "burger": "🍔", + "taco": "🌮", + "ramen": "🍜", + "sushi": "🍣", + "salad": "🥗", +} + +result = radial_menu( + data={"options": options, "selection": "burger"}, + default={"selection": "burger"}, + on_selection_change=lambda: None, + key="food_menu", +) + +if result.selection: + icon = options.get(result.selection, "") + st.write(f"You selected: **{icon} {result.selection.title()}**") +``` + +
+ +```markup +
+ + + + + +
+``` + + + +```css +.radial-menu { + position: relative; + display: inline-block; + font-family: var(--st-font); +} + +/* The circular selector button and menu items*/ +.menu-selector, +.menu-item { + width: 3.25rem; + height: 3.25rem; + border-radius: 50%; + border: 2px solid var(--st-border-color); + cursor: pointer; + background: var(--st-secondary-background-color); + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + font-size: 1.5rem; +} + +.menu-selector:hover { + transform: scale(1.05); + border-color: var(--st-primary-color); +} + +/* Overlay container */ +.menu-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + z-index: 100; + pointer-events: none; +} + +/* The ring of menu items */ +.menu-ring { + position: relative; + width: 13rem; + height: 13rem; + transform: scale(0); + opacity: 0; + transition: + transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), + opacity 0.2s ease; +} + +.menu-ring.open { + transform: scale(1); + opacity: 1; + pointer-events: auto; +} + +/* Menu items arranged in a circle */ +.menu-item { + --angle: calc(var(--i) * (360deg / var(--total)) - 90deg); + --radius: 4rem; + + background: var(--st-background-color); + position: absolute; + top: 50%; + left: 50%; + margin: -1.6125rem; + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))); +} + +.menu-item:hover { + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))) scale(1.15); + border-color: var(--st-primary-color); + background: var(--st-secondary-background-color); +} + +.menu-item.selected { + border-color: var(--st-primary-color); + background: var(--st-secondary-background-color); +} + +/* Backdrop when menu is open (must be outside .menu-overlay to avoid + its transform creating a containing block for position: fixed) */ +.menu-backdrop { + display: none; + position: fixed; + inset: 0; + background: var(--st-background-color); + opacity: 0; + transition: opacity 0.3s ease; + z-index: 99; +} + +.menu-backdrop.open { + display: block; + opacity: 0.7; +} + +/* Center decoration */ +.menu-ring::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 2rem; + height: 2rem; + transform: translate(-50%, -50%); + border-radius: 50%; + background: var(--st-secondary-background-color); + border: 2px dashed var(--st-border-color); + opacity: 0.6; + box-sizing: border-box; +} +``` + + + + + +```javascript +export default function ({ parentElement, data, setStateValue }) { + const selector = parentElement.querySelector("#selector"); + const selectorIcon = parentElement.querySelector("#selector-icon"); + const overlay = parentElement.querySelector("#overlay"); + const backdrop = parentElement.querySelector("#backdrop"); + const ring = parentElement.querySelector("#ring"); + + let isOpen = false; + const options = data?.options || {}; + let currentSelection = data?.selection || Object.keys(options)[0]; + const optionCount = Object.keys(options).length; + + // Set total item count for CSS angle calculation + ring.style.setProperty("--total", optionCount); + + // Create the menu items from options + Object.entries(options).forEach(([value, icon], index) => { + const button = document.createElement("button"); + button.className = "menu-item"; + button.dataset.value = value; + button.style.setProperty("--i", index); + button.textContent = icon; + + button.onclick = () => { + currentSelection = value; + updateDisplay(); + toggleMenu(); + setStateValue("selection", currentSelection); + }; + + ring.appendChild(button); + }); + + // Update the selector icon and highlight selected item + function updateDisplay() { + selectorIcon.textContent = options[currentSelection] || "?"; + + ring.querySelectorAll(".menu-item").forEach((item) => { + item.classList.toggle( + "selected", + item.dataset.value === currentSelection, + ); + }); + } + + // Calculate and apply viewport-safe position for the menu + function updatePosition() { + const selectorRect = selector.getBoundingClientRect(); + const menuRadius = ring.offsetWidth / 2; + const toolbarHeight = 60; // Streamlit toolbar height + + // Center of selector in viewport + const centerX = selectorRect.left + selectorRect.width / 2; + const centerY = selectorRect.top + selectorRect.height / 2; + + // Calculate overflow on each side (account for toolbar at top) + const overflowLeft = menuRadius - centerX; + const overflowRight = centerX + menuRadius - window.innerWidth; + const overflowTop = menuRadius - (centerY - toolbarHeight); + const overflowBottom = centerY + menuRadius - window.innerHeight; + + // Apply offset to keep menu in viewport + const offsetX = Math.max(0, overflowLeft) - Math.max(0, overflowRight); + const offsetY = Math.max(0, overflowTop) - Math.max(0, overflowBottom); + + overlay.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`; + } + + // Debounced resize handler for performance + let resizeTimeout; + function handleResize() { + clearTimeout(resizeTimeout); + resizeTimeout = setTimeout(updatePosition, 16); // ~60fps + } + + // Toggle menu open/closed + function toggleMenu() { + isOpen = !isOpen; + backdrop.classList.toggle("open", isOpen); + overlay.classList.toggle("open", isOpen); + ring.classList.toggle("open", isOpen); + + if (isOpen) { + updatePosition(); + window.addEventListener("resize", handleResize); + } else { + window.removeEventListener("resize", handleResize); + clearTimeout(resizeTimeout); + // Reset position after close animation completes (400ms matches CSS transition) + setTimeout(() => { + if (!isOpen) overlay.style.transform = ""; + }, 400); + } + } + + // Initialize display + updateDisplay(); + + // Attach click handlers + selector.onclick = toggleMenu; + backdrop.onclick = () => toggleMenu(); + + // Cleanup function + return () => { + clearTimeout(resizeTimeout); + window.removeEventListener("resize", handleResize); + }; +} +``` + + + +```python filename="streamlit_app.py" +import streamlit as st + +radial_menu = st.components.v2.component( + name="radial_menu", + html="...", + css="...", + js="...", +) + +st.header("Radial Menu Component") + +st.write("Click the button to open the menu. Select your favorite food!") + +options = { + "pizza": "🍕", + "burger": "🍔", + "taco": "🌮", + "ramen": "🍜", + "sushi": "🍣", + "salad": "🥗", +} + +result = radial_menu( + data={"options": options, "selection": "burger"}, + default={"selection": "burger"}, + on_selection_change=lambda: None, + key="food_menu", +) + +if result.selection: + icon = options.get(result.selection, "") + st.write(f"You selected: **{icon} {result.selection.title()}**") +``` + +## How it works + +### State values for selection + +When the user selects an item, `setStateValue("selection", currentSelection)` updates the persistent state. Unlike trigger values, this persists across reruns: + +```javascript +button.onclick = () => { + currentSelection = value; + updateDisplay(); + toggleMenu(); + setStateValue("selection", currentSelection); +}; +``` + +### Default values + +The `default` parameter ensures the component has a consistent initial state without triggering an unnecessary rerun: + +```python +result = radial_menu( + data={"options": options, "selection": "burger"}, + default={"selection": "burger"}, # Matches initial selection + on_selection_change=lambda: None, +) +``` + +### Dynamic element generation + +Menu items are created dynamically based on the options passed via `data`: + +```javascript +ring.style.setProperty("--total", optionCount); + +Object.entries(options).forEach(([value, icon], index) => { + const button = document.createElement("button"); + button.style.setProperty("--i", index); + // ... + ring.appendChild(button); +}); +``` + +### CSS-based circular positioning + +The CSS uses custom properties to calculate each item's angle: + +```css +.menu-item { + --angle: calc(var(--i) * (360deg / var(--total)) - 90deg); + --radius: 4rem; + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))); +} +``` + +### Backdrop for click-outside behavior + +The backdrop is a separate `
` positioned outside `.menu-overlay`. This is necessary because `.menu-overlay` uses a CSS `transform`, which would create a new containing block for any `position: fixed` descendants, preventing a backdrop pseudo-element from covering the full viewport. Clicking the backdrop closes the menu: + +```javascript +backdrop.onclick = () => toggleMenu(); +``` diff --git a/content/develop/concepts/custom-components/components-v2/examples/rich-data.md b/content/develop/concepts/custom-components/components-v2/examples/rich-data.md new file mode 100644 index 000000000..80eeac8f6 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/rich-data.md @@ -0,0 +1,109 @@ +--- +title: "Component example: Rich data" +slug: /develop/concepts/custom-components/components-v2/examples/rich-data +description: A component that receives different data types from Python including DataFrames, JSON, and base64 images. +keywords: custom components v2, example, data exchange, Arrow, DataFrame, JSON, base64 +--- + +# Component example: Rich data + +This is a component that receives various data types from Python, including an Arrow-serializable dataframe, a JSON-serializable dictionary, and a Base64-encoded image. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Passing data from Python via the `data` parameter and accessing it in JavaScript +- Automatic dataframe and JSON serialization +- Passing an image as a Base64-encoded string +- Using a placeholder in the component's HTML and dynamically updating it with received data + +## Complete code + +```python filename="streamlit_app.py" +import pandas as pd +import streamlit as st +import base64 + +# Create sample data +@st.cache_data +def create_sample_df(): + return pd.DataFrame( + { + "name": ["Alice", "Bob", "Charlie"], + "city": ["New York", "London", "Tokyo"], + } + ) + +df = create_sample_df() + +# Load an image and convert to b64 string +@st.cache_data +def load_image_as_base64(image_path): + with open(image_path, "rb") as img_file: + img_bytes = img_file.read() + return base64.b64encode(img_bytes).decode("utf-8") + +img_base64 = load_image_as_base64("favi.png") + +# Serialization is automatically handled by Streamlit components +data_component = st.components.v2.component( + "data_display", + html="""
Loading data...
""", + js=""" + export default function({ data, parentElement }) { + const container = parentElement.querySelector("#data-container"); + const df = data.df; + const userInfo = data.user_info; + const imgBase64 = data.image_base64; + container.innerHTML = ` +

Dataframe: ${df}

+

User Info: ${userInfo.name}

+ + `; + } + """, +) + +data_component( + data={ + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64, # Image as base64 string + } +) +``` + +## How it works + +### The `data` parameter + +When mounting a component, the `data` parameter passes information from Python to JavaScript. Streamlit automatically serializes the data: + +- DataFrames are converted to Apache Arrow format if passed directly to `data` or included as a value in a dictionary. +- Dictionaries, lists, strings, numbers, booleans are JSON-serialized. +- Bytes can be passed directly to `data`, but can't be passed as a value in a dictionary. + +### Accessing data in JavaScript + +The `data` property is available in the component function's argument object: + +```javascript +export default function ({ data, parentElement }) { + // data contains everything passed from Python + const df = data.df; + const userInfo = data.user_info; +} +``` + + + +DataFrames arrive as Arrow-formatted data on the frontend. In this simple example, they're converted to a string for display. For more sophisticated handling, you can use libraries like Apache Arrow JS to parse and manipulate the data. + + + +### Dynamic updates + +When `data` changes between reruns, your component's JavaScript function is called again with the new data. This enables reactive components that update based on Python state. For an example of a bidirectional reactive component, see the [Text input component example](/develop/concepts/custom-components/components-v2/examples/text-input). diff --git a/content/develop/concepts/custom-components/components-v2/examples/simple-button.md b/content/develop/concepts/custom-components/components-v2/examples/simple-button.md new file mode 100644 index 000000000..b792fba46 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/simple-button.md @@ -0,0 +1,78 @@ +--- +title: "Component example: Simple button" +slug: /develop/concepts/custom-components/components-v2/examples/simple-button +description: An interactive button component that sends trigger values to Python when clicked. +keywords: custom components v2, example, button, trigger values, setTriggerValue, callbacks +--- + +# Component example: Simple button + +This is an interactive button that sends events to Python. It demonstrates basic frontend-to-backend communication. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Component registration with HTML, CSS, and JavaScript +- One-time trigger values sent from JavaScript with `setTriggerValue()` +- Callback functions using the `on__change` naming pattern +- Accessing trigger values from the component's return object + +## Complete code + +```python filename="streamlit_app.py" +import streamlit as st + +if "click_count" not in st.session_state: + st.session_state.click_count = 0 + + +def handle_button_click(): + st.session_state.click_count += 1 + + +button_component = st.components.v2.component( + "simple_button", + html="""""", + css=""" + button { + border: none; + padding: .5rem; + border-radius: var(--st-button-radius); + background-color: var(--st-primary-color); + color: white; + } + """, + js=""" + export default function(component) { + const { setTriggerValue, parentElement } = component; + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); + }; + } + """, +) + +result = button_component(on_action_change=handle_button_click) + +if result.action: + st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") +``` + +## How it works + +### Trigger values + +When the button is clicked, `setTriggerValue("action", "button_clicked")` sends a one-time event to Python. The makes the component's `"action"` attribute return the string `"button_clicked"` in Python. + +### Callback registration + +The `on_action_change` parameter registers a callback that runs when the trigger fires. The callback name follows the pattern `on__change`. Always register a callback for trigger values, even an empty one like `lambda: None`. This ensures the result object always has an attribute for the trigger. In this example, if a callback isn't defined, `"action"` won't exist as a result attribute until the trigger fires. This can cause an `AttributeError` if you try to access it before the trigger fires. + +In this example, the callback is used to increment the value of `st.session_state.click_count`. This is a simple way to track the number of times the button has been clicked. Alternatively, you could use a state value in your component to track the number of clicks. For an example of a component that uses a state value to track the number of clicks, see the [interactive counter](/develop/concepts/custom-components/components-v2/examples/interactive-counter) example. + +### Accessing the trigger + +The trigger value is accessible via `result.action`. Trigger values are transient, just like `st.button()` values. However, you can configure triggers to return different data types and values for different events. In this example, the `"action"` attribute returns the string `"button_clicked"` when the button is clicked and `None` otherwise. You can create complex components that return different values to indicate different user actions. diff --git a/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md b/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md new file mode 100644 index 000000000..9569515c5 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md @@ -0,0 +1,118 @@ +--- +title: "Component example: Simple checkbox" +slug: /develop/concepts/custom-components/components-v2/examples/simple-checkbox +description: A simple checkbox component that sends persistent state values to Python. +keywords: custom components v2, example, checkbox, state values, setStateValue, callbacks +--- + +# Component example: Simple checkbox + +This is a simple checkbox that sends a state value to Python when toggled, demonstrating persistent state communication. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Persistent state values sent from JavaScript with `setStateValue()` +- Callback functions with the `on__change` naming pattern +- Initializing a stateful component with the `data` and `default` parameters +- Using font from the app's theme +- Accessing state values from the component's return object + +## Complete code + +```python filename="streamlit_app.py" +import streamlit as st + + +def handle_checkbox_change(): + st.toast(f"Checkbox is now: {st.session_state.my_checkbox.checked}") + + +checkbox_component = st.components.v2.component( + "simple_checkbox", + html=""" + + """, + css=""" + .checkbox-container { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--st-font); + color: var(--st-text-color); + cursor: pointer; + } + input[type="checkbox"] { + width: 1.25rem; + height: 1.25rem; + accent-color: var(--st-primary-color); + cursor: pointer; + } + """, + js=""" + export default function({ parentElement, data, setStateValue }) { + const checkbox = parentElement.querySelector("#checkbox"); + + // Initialize from data + checkbox.checked = data?.checked ?? false; + + // Send state on change + checkbox.addEventListener("change", () => { + setStateValue("checked", checkbox.checked); + }); + } + """, +) + +initial_state = True + +result = checkbox_component( + data={"checked": initial_state}, + default={"checked": initial_state}, + on_checked_change=handle_checkbox_change, + key="my_checkbox", +) + +st.write(f"Current state: {'Enabled' if result.checked else 'Disabled'}") +``` + +## How it works + +### State values vs trigger values + +This example uses `setStateValue()` instead of `setTriggerValue()`. State and trigger values have the following key differences: + +- State values persist across reruns. Use state values for data that represents the current state of your component. +- Trigger values reset after each rerun. Use trigger values for one-time events like button clicks. + +### The `default` parameter + +The `default` parameter sets the initial value in Python without triggering a rerun: + +```python +result = checkbox_component( + data={"checked": initial_state}, + default={"checked": initial_state}, # Matches data + on_checked_change=handle_checkbox_change, +) +``` + +To set a default value for a state value, you must also have an accompanying callback function, even if it's an empty one like `lambda: None`. The `on_checked_change` callback ensures that the `"checked"` attribute is available for the component and the `default` parameter sets its initial value in Python. + +If you set a default value without an associated callback function, the mounting command will raise an error because it won't recognize the state name. Conversely, if you declare a callback function without a default value, the state will be `None` until the component calls `setStateValue()` from JavaScript. + +### The `data` parameter + +The JavaScript code reads `data.checked` to initialize the checkbox state on the frontend: + +```javascript +checkbox.checked = data?.checked ?? false; +``` + +This is a common pattern for initializing component state: `default` initializes the state in Python and `data` is used to intialize the state on the frontend. Some components might not have more than one initial state, in which case you can use the `default` parameter alone. diff --git a/content/develop/concepts/custom-components/components-v2/examples/text-input.md b/content/develop/concepts/custom-components/components-v2/examples/text-input.md new file mode 100644 index 000000000..52a74aa64 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/text-input.md @@ -0,0 +1,219 @@ +--- +title: "Component example: Text input" +slug: /develop/concepts/custom-components/components-v2/examples/text-input +description: A text input component demonstrating bidirectional communication with programmatic updates from Python. +keywords: custom components v2, example, text input, bidirectional communication, session state, feedback loop +--- + +# Component example: Text input + +This is a text input component that demonstrates full bidirectional communication, including programmatic updates from Python. + + + +## Key concepts demonstrated + +This component demonstrates the following concepts: + +- Mounting a component with a key and reading component state from Session State +- Wrapping a component's raw mounting command to create a user-friendly mounting command +- Programmatic updates from Python via the `data` parameter +- Syncing frontend state without interrupting user input + +## Complete code + +For easy copying, expand the complete code below. For easier reading, the HTML and JavaScript are shown separately. + + + +```python filename="streamlit_app.py" +import streamlit as st + +HTML = """ + + +""" + +JS = """ + export default function(component) { + const { setStateValue, parentElement, data } = component; + + const label = parentElement.querySelector('label'); + label.innerText = data.label; + + const input = parentElement.querySelector('input'); + if (input.value !== data.value) { + input.value = data.value ?? ''; + }; + + input.onkeydown = (e) => { + if (e.key === 'Enter') { + setStateValue('value', e.target.value); + } + }; + + input.onblur = (e) => { + setStateValue('value', e.target.value); + }; + } +""" + +textbox_component = st.components.v2.component( + "simple_textbox", + html=HTML, + js=JS, +) + + +def textbox_component_wrapper( + label, *, default="", key=None, on_change=lambda: None +): + component_state = st.session_state.get(key, {}) + value = component_state.get("value", default) + data = {"label": label, "value": value} + result = textbox_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result + + +if st.button("Hello World"): + st.session_state["my_textbox"]["value"] = "Hello World" +if st.button("Clear text"): + st.session_state["my_textbox"]["value"] = "" +result = textbox_component_wrapper( + "Enter something", + default="I love Streamlit!", + key="my_textbox", +) + +st.write("Result:", result) +st.write("Session state:", st.session_state) +``` + + + +```markup + + +``` + +```javascript +export default function (component) { + const { setStateValue, parentElement, data } = component; + + const label = parentElement.querySelector("label"); + label.innerText = data.label; + + const input = parentElement.querySelector("input"); + if (input.value !== data.value) { + input.value = data.value ?? ""; + } + + input.onkeydown = (e) => { + if (e.key === "Enter") { + setStateValue("value", e.target.value); + } + }; + + input.onblur = (e) => { + setStateValue("value", e.target.value); + }; +} +``` + +```python filename="streamlit_app.py" +import streamlit as st + +textbox_component = st.components.v2.component( + "simple_textbox", + html="...", + js="...", +) + + +def textbox_component_wrapper( + label, *, default="", key=None, on_change=lambda: None +): + component_state = st.session_state.get(key, {}) + value = component_state.get("value", default) + data = {"label": label, "value": value} + result = textbox_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result + + +if st.button("Hello World"): + st.session_state["my_textbox"]["value"] = "Hello World" +if st.button("Clear text"): + st.session_state["my_textbox"]["value"] = "" +result = textbox_component_wrapper( + "Enter something", + default="I love Streamlit!", + key="my_textbox", +) + +st.write("Result:", result) +st.write("Session state:", st.session_state) +``` + +## How it works + +### The wrapper function pattern + +The wrapper function creates a reusable interface for your component: + +```python +def textbox_component_wrapper( + label, *, default="", key=None, on_change=lambda: None +): + # Read current state from Session State + component_state = st.session_state.get(key, {}) + value = component_state.get("value", default) + + # Pass current value to component + data = {"label": label, "value": value} + result = textbox_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result +``` + +This pattern: + +1. Reads the current value from Session State (falling back to `default`) +2. Passes the value to the component via `data` +3. Returns the result for the caller to use + +### Syncing without interruption + +The JavaScript checks if the value has actually changed before updating: + +```javascript +if (input.value !== data.value) { + input.value = data.value ?? ""; +} +``` + +This prevents the input from being overwritten while the user is typing. Without this check, each rerun would reset the input to the last committed value. + +### Programmatic updates + +Buttons can modify Session State directly: + +```python +if st.button("Hello World"): + st.session_state["my_textbox"]["value"] = "Hello World" +``` + +On the next rerun, the wrapper reads this new value from Session State and passes it to the component via `data`. The JavaScript then updates the input field. diff --git a/content/develop/concepts/custom-components/components-v2/mount.md b/content/develop/concepts/custom-components/components-v2/mount.md new file mode 100644 index 000000000..6edf72125 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/mount.md @@ -0,0 +1,301 @@ +--- +title: Component mounting +slug: /develop/concepts/custom-components/components-v2/mount +description: Learn how to mount custom v2 components in your Streamlit app, pass data, handle callbacks, and access component values. +keywords: custom components v2, component mounting, ComponentRenderer, key, data, default, callbacks, component values +--- + +# Component mounting + +After registering your component, you must mount your component in your Streamlit app. This creates a specific instance of the component and is equivalent to calling a native Streamlit command like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output. + +## Basic examples + +### Hello world + +This is the [hello world](/develop/concepts/custom-components/components-v2/examples/hello-world) component shown in the quickstart guide. It's a static component that displays "Hello, World!" using the app's theme colors. + +```python +import streamlit as st + +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }", +) + +hello_component() # Mount the component +hello_component(key="second_instance") # Mount another instance of the component +``` + +### Simple button + +This is the [simple button](/develop/concepts/custom-components/components-v2/examples/simple-button) component shown in the quickstart guide. It's an interactive button that sends a trigger value to Python when clicked. + +```python +import streamlit as st + +button_component = st.components.v2.component( + name="simple_button", + html="""""", + css="button { background: var(--st-primary-color); color: white; }", + js=""" + export default function(component) { + const { setTriggerValue, parentElement } = component; + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); + }; + } + """ +) + +result = button_component(on_action_change=lambda: None) +``` + +### Simple checkbox + +This is the [simple checkbox](/develop/concepts/custom-components/components-v2/examples/simple-checkbox) component shown in the quickstart guide. It's a simple checkbox that reports a stateful value to Python when toggled. + +```python +import streamlit as st + + +def handle_checkbox_change(): + st.toast(f"Checkbox is now: {st.session_state.my_checkbox.checked}") + + +checkbox_component = st.components.v2.component( + "simple_checkbox", + html=""" + + """, + css=""" + .checkbox-container { + display: flex; + align-items: center; + gap: 0.5rem; + font-family: var(--st-font); + color: var(--st-text-color); + cursor: pointer; + } + input[type="checkbox"] { + width: 1.25rem; + height: 1.25rem; + accent-color: var(--st-primary-color); + cursor: pointer; + } + """, + js=""" + export default function({ parentElement, data, setStateValue }) { + const checkbox = parentElement.querySelector("#checkbox"); + + // Initialize from data + checkbox.checked = data?.checked ?? false; + + // Send state on change + checkbox.addEventListener("change", () => { + setStateValue("checked", checkbox.checked); + }); + } + """, +) + +initial_state = True + +result = checkbox_component( + data={"checked": initial_state}, + default={"checked": initial_state}, + on_checked_change=handle_checkbox_change, + key="my_checkbox", +) + +st.write(f"Current state: {'Enabled' if result.checked else 'Disabled'}") +``` + +## Mounting parameters + +All mounting parameters are keyword-only and optional. The available parameters are documented in the [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) class. + +### Component identity (`key`) + +Components use the Python `key` parameter in the same manner as widgets. For a detailed overview of keys in widgets, see [Understanding widget behavior](/develop/concepts/architecture/widget-behavior#keys-help-distinguish-widgets-and-access-their-values). + +Just like widgets, components have internally computed identities that help Streamlit match component mounting commands to their frontend instances. + +- If you pass a key when you mount your component, Streamlit will update the existing frontend element when other parameters change. +- If you don't pass a key when you mount your component, Streamlit will create a new frontend element when other parameters change. This will reset the component's state. + +Additionally, you must use keys to disambiguate between otherwise identical instances of the same component. + +```python +# Multiple instances of the same component +result1 = my_component(key="first_instance") +result2 = my_component(key="second_instance") +``` + +In the hello world example, two instances of the same component are mounted. Because there is no other data passed to the instances, for Streamlit to compute unique identities, at least one of the instances must be given a key: + +```python +hello_component() # Mount the component +hello_component(key="second_instance") # Mount another instance of the component +``` + +If you remove the key from the second instance, you would get a `StreamlitDuplicateElementID` error. This is the component's equivalent to a `DuplicateWidgetID` error. + + + +The `key` property available in JavaScript in the `FrontendRendererArgs` type isn't the same as the Python `key` parameter. On the frontend, the JavaScript `key` is a dynamically generated identifier that is only usable for a specific instance of the component. For example, the JavaScript `key` will change if you mount a component, navigate away from the page, and then navigate back to remount it. + + + +### State and trigger callbacks (`on__change` or `on__change`) + +For each state and trigger value for your component, you must pass a callback function to the component mounting command. For example, if you have a trigger named `"click"`, then you must pass a callback function to the keyword argument `on_click_change`. + +```python +result = my_component(on_action_change=lambda: None) +``` + +In general, to create a callback's keyword argument name, prefix your state or trigger name with `on_` and then suffix it with `_change`. If you don't need to execute any Python logic in a callback, you can pass `lambda: None` as the callback function. + +Component callback functions work similarly to [widget callback functions](/develop/concepts/architecture/widget-behavior#order-of-operations). For components, `setStateValue()` and `setTriggerValue()` start the Python rerun process from the component's JavaScript code. However, there are two important distinctions compared to widget callback functions: + +- Because components can have multiple states and triggers, a single component instance can have multiple callbacks and also execute multiple callbacks in one script rerun. This is explained in more detail on the next page, [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate). +- Component callback functions play an important role in shaping the component's result, which is accessed through the component's return value and Session State. + +By passing a callback function for each of your component's state and trigger values, you ensure that all of your component's state and trigger values are consistently available in the component's result object: + +- In the simple button example, the callback is set with `on_action_change=lambda: None`. Because the callback is defined, even trivially, the result returned by the component will always have an `action` attribute. This attribute has a value of `None` until the button is clicked. +- In the simple checkbox example, the callback is set with `on_checked_change=handle_checkbox_change`. Because the callback is defined, the result returned by the component will always have a `checked` attribute. The default value is configured with the `default` parameter and is `True`. Additionally, `handle_checkbox_change` executes custom logic each time the checkbox is toggled. + + + +The `None` default value for triggers isn't related to using `lambda: None` as the callback function. A trigger always has a default value of `None`, regardless of what callback function it has. + + + +### Customizing and updating an instance (`data` and `default`) + +In a component mounting command, there are two parameters that you can use to customize and update a component instance: `data` and `default`. + +The `data` parameter passes information from Python to your component's frontend. It supports JSON-serializable, Arrow-serializable, and raw bytes data. Commonly this is a single value or a dictionary of values that you retrieve in your JavaScript function. + +```python +result = my_component( + data={ + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64 # Image as base64 string + } +) +``` + +DataFrames are automatically serialized using Apache Arrow format, which provides efficient transfer and preserves data types. On the frontend, you can work with the Arrow data directly or convert it to other formats as needed. + +The `default` parameter sets the initial values for your component's state _in Python_. This is a dictionary where each key is a state name. Each state name has an accompanying callback function passed as a keyword argument named `on__change`. Because `default` only sets the initial value in Python, you must appropriately pass data to the component's `data` parameter to ensure that the component is consistent with its intended initial state. + +#### Initialize component state + +In general, the `default` parameter is used to avoid a rerun of the script when the component is mounted. Otherwise, your component might need to immediately call `setStateValue()` when it's mounted to inform Python of its initial state. Unnecessary reruns are inefficient and might increase the chance of visual flickering. + +The simple checkbox example demonstrates how to use the `default` parameter to avoid a rerun of the script when the component is mounted. An initial value of `True` is set for the `"checked"` state: + +```python +initial_state = True + +result = checkbox_component( + data={"checked": initial_state}, + default={"checked": initial_state}, + on_checked_change=handle_checkbox_change, + key="my_checkbox", +) +``` + +In the component's JavaScript function, the initial DOM state is set from the `"checked"` key in the `data` parameter: + +```javascript +export default function ({ parentElement, data, setStateValue }) { + const checkbox = parentElement.querySelector("#checkbox"); + + // Initialize from data + checkbox.checked = data?.checked ?? false; + + // Send state on change + checkbox.addEventListener("change", () => { + setStateValue("checked", checkbox.checked); + }); +} +``` + +In the previous example, if you remove `default={"checked": initial_state},` from the Python code, then the initial state of the `"checked"` key would be `None`, which would be out of sync with the frontend until the first user interaction. In this case, you would have to add `setStateValue("checked", data?.checked ?? false);` to the JavaScript code to ensure that the initial state is set correctly. + +```diff +export default function({ parentElement, data, setStateValue }) { + const checkbox = parentElement.querySelector("#checkbox"); + + // Initialize from data + checkbox.checked = data?.checked ?? false; ++ setStateValue("checked", data?.checked ?? false); + + // Send state on change + checkbox.addEventListener("change", () => { + setStateValue("checked", checkbox.checked); + }); +} +``` + +This causes an unnecessary rerun of the script when the component is mounted, which is why it's recommended to use the `default` parameter instead. + +### Layout control (`width` and `height`) + +To make your component compatible with the Streamlit layout system, you can pass `width` and `height` parameters to your component mounting command. These parameters match the same width and height parameters used in other Streamlit commands. Streamlit wraps your component in a `
` element and updates its `width` and `height` properties so that it behaves like other Streamlit elements. + +```python +result = my_component( + width="stretch", # Full width + height=400 # Fixed height +) +``` + +On the frontend, because Streamlit will size the `
` wrapper element correctly, it's generally recommended to set your component's CSS to `width: 100%; height: 100%`. If your component needs to know its exact measurements at runtime in JavaScript, you can use a [`ResizeObserver`](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) to get that information dynamically. + +### Theming and styling (`isolate_styles`) + +Custom Components v2 provides style isolation options to control whether or not to sandbox your component in a shadow root. This is useful to prevent your component's styles from leaking to the rest of the page and to prevent the page's styles from leaking into your component. By default, Streamlit uses a shadow root for your component. + +```python +result = my_component( + isolate_styles=True # Default behavior uses a shadow root +) +``` + +For more information about theming and styling, see the [Theming and styling](/develop/concepts/custom-components/components-v2/theming) guide. + +## Accessing component values + +You can access the state and trigger values of a component through the mounting command's return value. Alternatively, if you mounted your component with a key, you can access the component values through Session State. + +### Component return value + +Component mounting commands return a [`BidiComponentResult`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) object that provides access to state and trigger values. You can access each state or trigger value as an attribute of the result object. + +```python +result = my_component(on_action_change=lambda: None) +st.write(result.action) +``` + +### Component values in Session State + +If you mounted your component with a key, you can access the component values through Session State. The component's result object is stored in Session State under the key you provided. + +```python +result = my_component(on_action_change=lambda: None, key="my_key") +st.write(st.session_state.my_key.action) +``` + +### State vs trigger behavior + +State and trigger values have different behavior in relation to reruns. State values persist across reruns, while trigger values are transient and reset after each rerun. For more information about state and trigger values, see the [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) guide. diff --git a/content/develop/concepts/custom-components/components-v2/package-based.md b/content/develop/concepts/custom-components/components-v2/package-based.md new file mode 100644 index 000000000..d7597b701 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/package-based.md @@ -0,0 +1,402 @@ +--- +title: Package-based components +slug: /develop/concepts/custom-components/components-v2/package-based +description: Learn how to build complex Custom Components v2 using package-based development with TypeScript, modern build tools, and external dependencies. +keywords: custom components v2, package-based components, TypeScript, build tools, Vite, pyproject.toml, npm packages, component distribution, component template +--- + +# Package-based components + +While inline components are perfect for rapid prototyping, package-based components provide the full power of modern frontend development. This approach is ideal for complex components that require TypeScript, external dependencies, build optimization, or distribution as Python packages. + +Choose package-based components when you need one of the following features: + +- **TypeScript support**: Type safety and better developer experience +- **External dependencies**: React, D3, Chart.js, or other npm packages +- **Build optimization**: Code splitting, minification, and bundling +- **Team development**: Proper tooling, testing, and collaboration workflows +- **Distribution**: Publishing components as Python packages on PyPI +- **Complex logic**: Multi-file projects with organized code structure + +## Get started with the template + +The fastest way to create a package-based component is with the official [component template](https://github.com/streamlit/component-template). It generates a complete project with all the configuration, build tooling, and boilerplate you need. + +### Prerequisites + +- Python >= 3.10 +- [Node.js](https://nodejs.org/) >= 24 (LTS) +- [uv](https://docs.astral.sh/uv/) (recommended Python package manager) + +### Generate a project + +1. Navigate to the directory where you want to create your project, then run the following command: + + ```bash + uvx --from cookiecutter cookiecutter gh:streamlit/component-template --directory cookiecutter/v2 + ``` + + The generator will create a new subdirectory for your project. + +2. Answer the generator's questions (project name, author, license) and choose between two frontend frameworks: + - React + TypeScript is best for components with complex, state-driven UIs. + - Pure TypeScript is best for lightweight components without framework overhead. + +### Run in development mode + +To run your component in development mode, you need to install dependencies and start two simultaneous processes from separate terminals. + +3. Navigate to the project root directory (e.g. `my-component/`) and install the Python package in editable mode: + + ```bash filename="TERMINAL A" + cd my-component + uv pip install -e . + ``` + +4. Navigate to the frontend directory and start the dev build watcher: + + ```bash filename="TERMINAL A" + cd my-component/my_component/frontend + npm install + npm run dev + ``` + +5. In a second terminal, navigate to the project root and run your Streamlit app: + + ```bash filename="TERMINAL B" + cd my-component + streamlit run example.py + ``` + +The `npm run dev` command watches for changes to your frontend code and rebuilds automatically. When you make changes, refresh your Streamlit app to see them. + +## Understanding the project structure + +Whether you use the template or create a project manually, a package-based component follows this structure: + +```none hideHeader +my-component/ +├── pyproject.toml # Python package configuration +├── example.py # Example Streamlit app +└── my_component/ + ├── __init__.py # Python API and component registration + ├── pyproject.toml # Streamlit component metadata + └── frontend/ + ├── build/ # Built frontend assets (generated) + │ └── index-.js + ├── src/ # Frontend source code + │ └── index.ts # Main TypeScript entry point + ├── package.json # Frontend dependencies and scripts + ├── tsconfig.json # TypeScript configuration + └── vite.config.ts # Build tool configuration +``` + +The rest of this page explains each piece of this structure. If you generated your project from the template, this will help you understand what the template created and how to customize it. + +### Top-level `pyproject.toml` + +The root `pyproject.toml` configures your Python package for distribution. For more information, see the [Python Packaging User Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/). + +```toml +[build-system] +requires = ["setuptools>=77.0.3", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "streamlit-custom-component" +version = "0.0.1" +description = "Streamlit component that allows you to do X" +requires-python = ">=3.10" +dependencies = ["streamlit >= 1.51"] + +[tool.setuptools.packages.find] +include = ["my_component*"] + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +my_component = ["frontend/build/**/*", "pyproject.toml"] +``` + +The key sections are: + +- `[project]`: Package metadata. The `name` field is what users pass to `pip install`. +- `[tool.setuptools.packages.find]`: Tells setuptools to automatically find your component package by matching the `my_component*` pattern. +- `[tool.setuptools.package-data]`: Ensures the built frontend assets (`frontend/build/**/*`) and the inner `pyproject.toml` are included when the package is built into a wheel. + +### Component-level `pyproject.toml` + +Inside your component module, a second `pyproject.toml` registers your component with Streamlit and specifies where the frontend assets live: + +```toml +[project] +name = "streamlit-custom-component" +version = "0.0.1" + +[[tool.streamlit.component.components]] +name = "my_component" +asset_dir = "frontend/build" +``` + +When you start a Streamlit app, Streamlit scans all installed packages for component metadata like this. For each component it finds, Streamlit serves the contents of the `asset_dir` directory. This makes it possible to refer to JavaScript bundles, images, and other assets within your component. + +The `project.name` should match the name of your package as installed. + + + +The `asset_dir` path is relative to the component-level `pyproject.toml` file. All files and subdirectories within the asset directory will be served publicly by Streamlit and won't be protected by any logical restrictions in your app. Don't include sensitive information in your component's asset directory. + + + +### Frontend configuration + +The `frontend/` directory contains all frontend source code and configuration: + +- `package.json` defines your frontend dependencies and build scripts. +- `tsconfig.json` configures the TypeScript compiler. +- `vite.config.ts` configures [Vite](https://vite.dev/) to build your component as an ES module library with hashed filenames. +- `src/index.ts` is the main TypeScript entry point for your component. + +For clarity, the following subsections highlight the most important settings and patterns for Streamlit. + +```json filename="package.json" +{ + ... + "scripts": { + "build": "npm run clean && npm run typecheck && npm run build:frontend:production", + "build:frontend:production": "cross-env NODE_ENV=production vite build", + "clean": "rimraf build", + "dev": "cross-env NODE_ENV=development vite build --watch", + "typecheck": "tsc --noEmit" + }, + ... + "dependencies": { + "@streamlit/component-v2-lib": "^0.2.0" + }, + ... +} +``` + +`package.json` includes the two most important scripts you will use during development and production: + +- `npm run dev` watches for changes and rebuilds during development. +- `npm run build` creates an optimized production build with hashed filenames. + +The [`@streamlit/component-v2-lib`](/develop/api-reference/custom-components/component-v2-lib) package provides the TypeScript type definitions for the component API. Your `src/index.ts` entry point imports types like [`FrontendRenderer`](/develop/api-reference/custom-components/component-v2-lib-frontendrenderer) and [`FrontendRendererArgs`](/develop/api-reference/custom-components/component-v2-lib-frontendrendererargs) from this package: + +```typescript filename="src/index.ts" +import { + FrontendRenderer, + FrontendRendererArgs, +} from "@streamlit/component-v2-lib"; + +export type FrontendState = { + num_clicks: number; +}; + +const MyComponent: FrontendRenderer = (args) => { + const { parentElement, data, setStateValue } = args; + // ... +}; + +export default MyComponent; +``` + + + +Use `FrontendRenderer` and `FrontendRendererArgs` directly, and extend them with your own generic type parameters for `FrontendState` and data shapes. This keeps your component's type signatures consistent with the Streamlit runtime and ensures you benefit from any upstream type improvements. + + + +```typescript filename="vite.config.ts" +{ + base: "./", + build: { + outDir: "build", + lib: { + entry: "./src/index.ts", + formats: ["es"], + fileName: "index-[hash]", + }, + ... + }, +} +``` + +The following Vite settings are particularly significant for Streamlit: + +- `base: "./"` tells Vite to use relative paths so assets resolve correctly when served by Streamlit. +- `outDir: "build"` outputs built files to `frontend/build/`. This must match the `asset_dir` in your component-level `pyproject.toml`. +- `formats: ["es"]` configures Vite to output an ES module, which is the format Streamlit expects when loading your component. +- `fileName: "index-[hash]"` tells Vite to include a content hash in the output filename (e.g. `index-a1b2c3d4.js`) for cache busting. You reference these with glob patterns in the Python API. + +### The Python API + +The `__init__.py` file registers your component and optionally provides a wrapper function: + +```python +import streamlit as st + +out = st.components.v2.component( + "streamlit-custom-component.my_component", + js="index-*.js", + html=' ', +) + +def on_num_clicks_change(): + pass + +def my_component(name, key=None): + component_value = out( + key=key, + default={"num_clicks": 0}, + data={"name": name}, + on_num_clicks_change=on_num_clicks_change, + ) + return component_value +``` + +A few things to note: + +- The first argument to `st.components.v2.component()` is a qualified name: `"."`. This matches the `project.name` in your top-level `pyproject.toml` and the `name` in your component-level `pyproject.toml`. +- The `js` parameter uses a glob pattern (`"index-*.js"`) to match the hashed build output. +- The wrapper function (`my_component`) provides a clean API for users of your component. It's optional but recommended. + +For more about registration and mounting, see [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount). + +## Glob pattern support + +A [glob pattern]() is a string with wildcards that matches filenames. For example, `index-*.js` matches any file whose name starts with `index-` and ends with `.js`. Package-based components use glob patterns to reference build outputs with hashed filenames. + +### Why use glob patterns? + +Modern build tools like Vite include a content hash in output filenames. The hash changes whenever the file content changes, which ensures browsers load the latest version instead of serving a stale cached copy: + +``` +frontend/build/ +├── index-a1b2c3d4.js # Hashed JavaScript bundle +└── styles-e5f6g7h8.css # Hashed CSS file (if applicable) +``` + +### Glob resolution rules + +1. **Pattern matching**: `index-*.js` matches `index-a1b2c3d4.js` +2. **Single file requirement**: A pattern must resolve to exactly one file. +3. **Security**: Matched files must be within the `asset_dir`. +4. **Relative paths**: Patterns are resolved relative to `asset_dir`. + +### Example usage + +```python +component = st.components.v2.component( + name="my_component", + js="index-*.js", # Matches index-.js + css="styles-*.css", # Matches styles-.css +) +``` + + + +If a glob pattern matches zero files or multiple files, Streamlit raises a clear error message to help you debug the issue. + + + +## Development workflow + +### Development mode + +During development, the `npm run dev` command watches your source files and rebuilds on changes. You need two terminals: + +```bash +# Terminal 1: Watch and rebuild frontend +cd my_component/frontend +npm run dev + +# Terminal 2: Run your Streamlit app +streamlit run example.py +``` + +After the frontend rebuilds, refresh your Streamlit app to see the changes. + +### Build for production + +When you're ready to distribute your component, create an optimized production build: + +```bash +cd my_component/frontend +npm install +npm run build +``` + +This generates hashed files in the `build/` directory that your glob patterns will match. + +### Build a Python wheel + +Package your component for distribution: + +```bash +uv build +``` + +This creates a wheel in the `dist/` directory that includes your compiled frontend assets. + +## Publishing your package + +For detailed instructions on publishing your component to PyPI, see [Publish a Component](/develop/concepts/custom-components/publish). + +The basic steps are: + +1. Build the frontend: `npm run build` (from the `frontend/` directory) +2. Build the wheel: `uv build` (from the project root) +3. Upload to PyPI: `uv publish` or `python -m twine upload dist/*` + +After publishing, users can install your component with: + +```bash +pip install streamlit-custom-component +``` + +## Best practices + +### Error handling + +Implement error handling in your frontend code to provide a good experience when something goes wrong: + +```typescript +export default function (component) { + try { + // Component logic + return () => { + // Cleanup + }; + } catch (error) { + console.error("Component error:", error); + component.parentElement.textContent = "Component failed to load"; + } +} +``` + +### Performance + +- Use code splitting for large dependencies. +- Implement lazy loading for heavy components. +- Optimize bundle sizes with tree shaking (Vite does this by default in production builds). + +### Documentation + +Provide comprehensive documentation for your component: + +- Python docstrings with parameter descriptions +- TypeScript interfaces for data shapes +- Usage examples +- A `README.md` in your project root + +## What's next? + +- Follow the tutorials to build your first package-based component: + - [Create a component with Pure TypeScript](/develop/tutorials/custom-components/template-typescript) + - [Create a component with React + TypeScript](/develop/tutorials/custom-components/template-react) +- Learn about [State vs triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) for interactive functionality. +- Explore [Theming and styling](/develop/concepts/custom-components/components-v2/theming) for beautiful components. +- Check out [Publishing components](/develop/concepts/custom-components/publish) for distribution strategies. diff --git a/content/develop/concepts/custom-components/components-v2/register.md b/content/develop/concepts/custom-components/components-v2/register.md new file mode 100644 index 000000000..1a18ff47b --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/register.md @@ -0,0 +1,157 @@ +--- +title: Component registration +slug: /develop/concepts/custom-components/components-v2/register +description: Learn how to register custom v2 components with HTML, CSS, and JavaScript to define their structure and behavior. +keywords: custom components v2, component registration, st.components.v2.component, HTML, CSS, JavaScript, FrontendRendererArgs +--- + +# Component registration + +When you register your component, you define what it looks like and how it behaves: + +- To define your component's HTML, CSS, and JavaScript, use [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). +- In your component's JavaScript code, to send and receive communications with Python, use the properties of the [`FrontendRendererArgs`](/develop/api-reference/custom-components/component-v2-lib-frontendrendererargs) type. +- In your component's CSS, to make your component theme-aware, use [CSS custom properties](/develop/concepts/custom-components/components-v2/theming#using-css-custom-properties). + +For simplicity and to help you get started with less prerequisite knowledge, this guide uses inline component development. For components that are larger, reusable, or distributed, you should use package-based development. The practical difference is that package-based components let you self-host assets and reference those assets, including component code, by a relative path. Contrastingly, inline components require you to pass raw HTML, CSS, and JavaScript code to your component registration command. + +After you learn about custom components with inline development, you can proceed to [package-based development](/develop/concepts/custom-components/components-v2/package-based) where you'll need to understand the basics of [Python packaging](https://packaging.python.org/en/latest/overview/), too. + +## Prerequisites + +To read this guide, you should be familiar with the basic syntax and concepts of HTML, CSS, and JavaScript. If you want to build an interactive component, you should also know how Streamlit's native widgets work. For more information about Streamlit widgets, see the [Widget behavior](/develop/concepts/architecture/widget-behavior) guide. + +## Basic examples + +Here are some basic examples of component registration. The parameters are explained in the next section. + +### Hello world + +This is the [hello world](/develop/concepts/custom-components/components-v2/examples/hello-world) component shown in the quickstart guide. It's a static component that displays "Hello, World!" using the app's theme colors. + +```python +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }", +) +``` + +### Simple button + +This is the [simple button](/develop/concepts/custom-components/components-v2/examples/simple-button) component shown in the quickstart guide. It's an interactive button that sends a trigger value to Python when clicked. + +```python +button_component = st.components.v2.component( + name="simple_button", + html="""""", + css="button { background: var(--st-primary-color); color: white; }", + js=""" + export default function(component) { + const { parentElement, setTriggerValue } = component; + + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); + }; + } + """ +) +``` + +## Registration parameters + +`name` is a unique identifier for your component. This is used internally by Streamlit to retrieve the HTML, CSS, and JavaScript code when a component instance is mounted. To avoid collisions, Streamlit prefixes component names with the modules they are imported from. For inline components that aren't imported, you must use unique names. + +`html`, `css`, and `js` are all optional parameters that define your component's markup, styling, and logic, respectively: + +- In the hello world example, `html` contains a single heading element and `css` styles it with the Streamlit theme's primary color. Because it's a static component, it doesn't need any JavaScript logic. +- In the simple button example, `html` contains a single button element, `css` styles it with the Streamlit theme's primary color, and the default function in `js` listens for clicks and sets a trigger value. + + + +A component must have either `html`, `js`, or both defined! You can't register a component with only CSS. If you only need to inject CSS, use `st.html()` instead. + + + +## JavaScript function requirements + +Your JavaScript code must export a default function that follows this exact signature: + +```javascript +export default function (component) { + // Your component logic here + + return () => { + // Cleanup logic (remove event listeners, clear timers, etc.) + }; +} +``` + +The `component` argument in your exported default function provides essential properties described in the [`FrontendRendererArgs`](/develop/api-reference/custom-components/component-v2-lib-frontendrendererargs) type. These properties are typically destructured into local variables for easier access: + +```javascript +export default function (component) { + const { name, key, data, parentElement, setStateValue, setTriggerValue } = + component; + // Your component logic here +} +``` + +- `name` (string): Component name from your Python registration. +- `key` (string): Unique identifier for a component instance. Use this to assist with tracking unique instances of your component in the DOM, especially if your component acts outside of its `parentElement`. +- `data` (any): All data passed from Python via the `data` parameter. Use this to customize a component instance or to receive feedback data from your Python backend. +- `parentElement` (HTMLElement of ShadowRoot): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements. +- `setStateValue` (function): JavaScript function to communicate stateful values to your Python backend. The first argument is the state key name, and the second argument is the value to set. +- `setTriggerValue` (function): JavaScript function to communicate event-based trigger values to your Python backend. The first argument is the trigger key name, and the second argument is the value to set. + +
+Component communication cycle +
+ + + +Don't directly overwrite or replace `parentElement.innerHTML`. If you do, you will overwrite the HTML, CSS, and JavaScript code that was registered with the component. If you need to inject content from `data`, do one of the following things: + +- Create a placeholder element within `html` to update. +- Append children to `parentElement`. + + + +## Using files for inline components + +For larger components, you can organize your code into separate files. However, for inline components, you must pass raw HTML, CSS, and JavaScript code when you register them. You can read the files and pass their contents to your inline component. For package-based components, you can pass file references instead. Typically, if you have a component that uses multiple files, package-based components is preferred over inline components. + +If you use multiple files for your inline component, use a context manager to read the files and pass their contents to your inline component. + + + +This pattern isn't recommended, especially for development, because you might need to restart your Streamlit server to see changes in your non-Python code. + + + +```none filename="Directory structure" +my_app/ +├── streamlit_app.py +└── component.html +``` + +```python +with open("my_component/my_html.html", "r") as f: + HTML = f.read() + +file_component = st.components.v2.component( + name="antipattern_file_based", + html=HTML +) +``` + +## Sending values to Python + +You can send state and trigger values to Python by calling `setStateValue()` or `setTriggerValue()` in your JavaScript code. For both functions, the first argument is the state or trigger name, and the second argument is the value to set. + +```javascript +setStateValue("count", count); +setTriggerValue("clicked", true); +``` + +Both `setStateValue()` and `setTriggerValue()` trigger a rerun of the script. On the next page, you'll learn about mounting your component, which includes defining callback functions for each state and trigger value. Custom components handle callbacks similarly to native Streamlit widgets, like `st.button()`. However, because components can have multiple states and triggers, a single component instance can have multiple callbacks and also execute multiple callbacks in one script rerun. This is explained in more detail on the next page, [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate). diff --git a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md new file mode 100644 index 000000000..53efdaaa8 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -0,0 +1,707 @@ +--- +title: State vs trigger values +slug: /develop/concepts/custom-components/components-v2/state-and-triggers +description: Learn the fundamental difference between state and trigger values in Custom Components v2, and when to use each approach for bidirectional communication. +keywords: custom components v2, state values, trigger values, bidirectional communication, component events, callback functions, setStateValue, setTriggerValue +--- + +# State versus trigger values + +Custom components v2 provides two distinct mechanisms for frontend-to-backend communication, each designed for different use cases. Understanding when to use state values versus trigger values is crucial for building effective interactive components. + +## Two communication patterns + +### State values: Persistent data + +**Purpose**: Represent the current "state" of your component that persists across reruns. + +**When to use**: For values that represent ongoing component state like current selections, input values, or configuration settings. + +State values have the following behavior: + +- Persist across Streamlit reruns. +- Accessible via direct property access on the result object and through Session State (when mounted with a key). +- Updated using `setStateValue(key, value)` in JavaScript. + +### Trigger values: Event-based communication + +**Purpose**: Signal one-time events or user interactions. + +**When to use**: For user actions like clicks, form submissions, or other discrete events. + +Trigger values have the following behavior: + +- Are transient and only available for one script rerun. +- Reset to `None` after the rerun completes. +- Accessible via direct property access on the result object and through Session State (when mounted with a key). +- Updated using `setTriggerValue(key, value)` in JavaScript. + +## Differences at a glance + +| Aspect | State values | Trigger values | +| :------------------ | :-------------------------------------------- | :--------------------------------------- | +| Persistence | Maintained across reruns | Only available for one rerun | +| Use case | Current component state | One-time events/actions | +| JavaScript function | `setStateValue(key, value)` | `setTriggerValue(key, value)` | +| Callback execution | Only if `setStateValue()` _changed_ the value | Every time `setTriggerValue()` is called | + +## State values in practice: Radial menu component + +State values are perfect for tracking the ongoing state of your component. The [Radial menu](/develop/concepts/custom-components/components-v2/examples/radial-menu) quickstart example demonstrates this pattern with a circular selection menu. When the user selects an item, the component calls `setStateValue` to persist their choice across reruns. + + + +The critical line in the component's JavaScript is the call to `setStateValue` inside the click handler. Because this is a state value, the selection persists across reruns. If the user interacts with another widget and triggers a rerun, `result.selection` still reflects their last choice: + +```javascript +button.onclick = () => { + currentSelection = value; + updateDisplay(); + toggleMenu(); + setStateValue("selection", currentSelection); +}; +``` + +On the Python side, the component is mounted with a `default` value that matches the initial selection. The `on_selection_change` callback registers the `"selection"` key so it appears on the result object: + +```python +result = radial_menu( + data={"options": options, "selection": "burger"}, + default={"selection": "burger"}, + on_selection_change=lambda: None, + key="food_menu", +) + +if result.selection: + icon = options.get(result.selection, "") + st.write(f"You selected: **{icon} {result.selection.title()}**") +``` + + + +## Trigger values in practice: Danger button component + +Trigger values are ideal for handling discrete user actions. The [Danger button](/develop/concepts/custom-components/components-v2/examples/danger-button) quickstart example demonstrates this pattern with a hold-to-confirm button. The component performs frontend validation by requiring the user to hold the button for a full two seconds and then calls `setTriggerValue` to fire a one-time trigger to Python. + + + +The key difference from a state value is that the trigger fires every time the action completes, and its value resets to `None` after the rerun. In this example, the `triggerAction` function only calls `setTriggerValue` after the full hold duration passes frontend validation: + +```javascript +function triggerAction() { + // ... reset animation state ... + + setTriggerValue("confirmed", true); + + // Reset after cooldown + setTimeout(() => { + // ... restore button to initial state ... + }, COOLDOWN_DURATION); +} +``` + +If the user releases early, `cancelHold()` resets the progress without ever calling `setTriggerValue`, so no rerun occurs: + +```javascript +function updateProgress() { + const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); + + if (progressPercent >= 1) { + triggerAction(); + } else { + animationFrame = requestAnimationFrame(updateProgress); + } +} +``` + +On the Python side, the `on_confirmed_change` callback runs each time the trigger fires. Because trigger values are transient, `result.confirmed` is only `True` during the rerun caused by the trigger—it reverts to `None` on subsequent reruns: + +```python +def on_delete_confirmed(): + st.session_state.deleted_items.append( + f"Deleted item #{len(st.session_state.deleted_items) + 1}" + ) + st.toast("Item permanently deleted!", icon="🗑️") + +result = danger_button( + key="danger_btn", + on_confirmed_change=on_delete_confirmed, +) +``` + + + +## Combining state and triggers + +Many components benefit from using both patterns together. The following example creates a stopwatch with laps. The component uses state values to track the time and whether the stopwatch is running. It also uses trigger values to track when the user starts a lap or resets the stopwatch. In summary, the component sets state and trigger values in the following events: + +- The user starts the stopwatch: + - `setStateValue("running", true)` + +- The user pauses the stopwatch: + - `setStateValue("running", false)` + - `setStateValue("elapsed", elapsedMs)` + +- The user records a lap: + - `setStateValue("laps", laps)` + - ``setTriggerValue("lap", { number: laps.length, time: elapsedMs, formatted: `${t.mins}:${t.secs}.${t.cents}` })`` + +- The user resets the stopwatch: + - `setStateValue("laps", [])` + - `setStateValue("elapsed", 0)` + - `setStateValue("running", false)` + - `setTriggerValue("reset", true)` + +```python +import streamlit as st + +st.title("Stopwatch with Laps") +st.caption("Combining state values (time, running) with trigger values (lap, reset)") + +# Track laps in Python +if "laps" not in st.session_state: + st.session_state.laps = [] + +stopwatch = st.components.v2.component( + name="stopwatch", + html=""" +
+
+ + + + +
+ 00 + : + 00 + . + 00 +
+
+ +
+ + + +
+ +
+
+ """, + css=""" + .stopwatch { + font-family: var(--st-font); + color: var(--st-text-color); + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + gap: 2rem; + } + + /* Ring Display */ + .display-ring { + position: relative; + width: 14rem; + height: 14rem; + } + + .ring-svg { + position: absolute; + inset: -.75rem; + padding: .75rem; + transform: rotate(-90deg); + overflow: visible; + } + + .ring-track, .ring-progress { + fill: none; + stroke-width: 6; + } + + .ring-track { + stroke: var(--st-secondary-background-color); + } + + .ring-progress { + stroke: var(--st-primary-color); + stroke-linecap: round; + stroke-dasharray: 565.5; + stroke-dashoffset: 565.5; + transition: stroke-dashoffset 0.1s linear; + filter: drop-shadow(0 0 8px var(--st-primary-color)); + } + + .ring-progress.running { + animation: glow 2s ease-in-out infinite; + } + + @keyframes glow { + 0%, 100% { opacity: 0.7; } + 50% { opacity: 1; } + } + + /* Time Display */ + .display { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + display: flex; + align-items: baseline; + gap: 2px; + font-family: var(--st-code-font); + font-size: 2.5rem; + font-weight: 700; + } + + .time-segment { + min-width: 2ch; + text-align: center; + letter-spacing: 0.05em; + } + + .separator { + opacity: 0.5; + } + + .time-segment.small, .separator.small { + font-size: 1.5rem; + font-weight: 500; + } + + .time-segment.small { + opacity: 0.7; + } + + /* Controls */ + .controls { + display: flex; + gap: 1rem; + align-items: center; + } + + .ctrl-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.75rem 1.25rem; + border: none; + border-radius: var(--st-button-radius); + cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + min-width: 5rem; + } + + .ctrl-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .ctrl-btn:hover:not(:disabled) { + transform: scale(1.05); + } + + .ctrl-btn.primary { + background: var(--st-primary-color); + color: white; + } + + .ctrl-btn.primary:hover:not(:disabled) { + filter: brightness(1.1); + } + + .ctrl-btn.secondary { + background: var(--st-secondary-background-color); + border: 1px solid var(--st-border-color); + } + + .ctrl-btn.secondary:hover:not(:disabled) { + border-color: var(--st-primary-color); + } + + .btn-icon { + font-size: 1.25rem; + line-height: 1; + } + + .btn-label { + font-size: 0.7rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.05em; + } + + /* Lap List */ + .lap-list { + width: 100%; + max-width: 280px; + display: flex; + flex-direction: column; + gap: 0.5rem; + max-height: 150px; + overflow-y: auto; + } + + .lap-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.5rem 1rem; + background: var(--st-secondary-background-color); + border-radius: var(--st-base-radius); + font-size: 0.85rem; + animation: slide-in 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + } + + @keyframes slide-in { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } + } + + .lap-number { + color: var(--st-primary-color); + font-weight: 600; + } + + .lap-time, .lap-delta { + font-family: var(--st-code-font); + font-size: 0.8rem; + opacity: 0.8; + } + + .lap-delta.fastest { + color: var(--st-green-color); + opacity: 1; + } + + .lap-delta.slowest { + color: var(--st-red-color); + opacity: 1; + } + """, + js=""" + export default function({ parentElement, data, setStateValue, setTriggerValue }) { + const minutes = parentElement.querySelector("#minutes"); + const seconds = parentElement.querySelector("#seconds"); + const centiseconds = parentElement.querySelector("#centiseconds"); + const ringProgress = parentElement.querySelector("#ring-progress"); + const startBtn = parentElement.querySelector("#start-btn"); + const lapBtn = parentElement.querySelector("#lap-btn"); + const resetBtn = parentElement.querySelector("#reset-btn"); + const lapList = parentElement.querySelector("#lap-list"); + + const CIRCUMFERENCE = 2 * Math.PI * 90; + + // Initialize from state or defaults + let elapsedMs = data?.elapsed || 0; + let isRunning = data?.running || false; + let laps = data?.laps || []; + let lastTimestamp = null; + let animationFrame = null; + + let lastMinute = Math.floor(elapsedMs / 60000); + let isTransitioning = false; + + function formatTime(ms) { + const totalSeconds = Math.floor(ms / 1000); + const mins = Math.floor(totalSeconds / 60); + const secs = totalSeconds % 60; + const cents = Math.floor((ms % 1000) / 10); + return { + mins: String(mins).padStart(2, "0"), + secs: String(secs).padStart(2, "0"), + cents: String(cents).padStart(2, "0") + }; + } + + function updateDisplay() { + const time = formatTime(elapsedMs); + minutes.textContent = time.mins; + seconds.textContent = time.secs; + centiseconds.textContent = time.cents; + + const currentMinute = Math.floor(elapsedMs / 60000); + const secondsInMinute = (elapsedMs % 60000) / 1000; + + // Arc length: 0 at second 0, full circle at second 60 + const arcLength = (secondsInMinute / 60) * CIRCUMFERENCE; + + // Detect minute boundary - quick fade transition + if (currentMinute > lastMinute && !isTransitioning) { + lastMinute = currentMinute; + isTransitioning = true; + + // Quick fade out + ringProgress.style.transition = "opacity 0.15s ease-out"; + ringProgress.style.opacity = "0"; + + setTimeout(() => { + // Reset to small arc while invisible + ringProgress.style.transition = "none"; + ringProgress.style.strokeDasharray = `${arcLength} ${CIRCUMFERENCE}`; + ringProgress.style.strokeDashoffset = 0; + + // Fade back in + requestAnimationFrame(() => { + ringProgress.style.transition = "opacity 0.15s ease-in"; + ringProgress.style.opacity = "1"; + + setTimeout(() => { + ringProgress.style.transition = ""; + isTransitioning = false; + }, 150); + }); + }, 150); + } + + // Normal ring update + if (!isTransitioning) { + ringProgress.style.strokeDasharray = `${arcLength} ${CIRCUMFERENCE}`; + ringProgress.style.strokeDashoffset = 0; + } + } + + function updateButtons() { + startBtn.querySelector(".btn-icon").textContent = isRunning ? "⏸" : "▶"; + startBtn.querySelector(".btn-label").textContent = isRunning ? "Pause" : "Start"; + startBtn.classList.toggle("running", isRunning); + ringProgress.classList.toggle("running", isRunning); + + lapBtn.disabled = !isRunning; + resetBtn.disabled = isRunning || elapsedMs === 0; + } + + function renderLaps() { + lapList.innerHTML = ""; + + if (laps.length === 0) return; + + // Calculate deltas and find fastest/slowest + const deltas = laps.map((lap, i) => { + return i === 0 ? lap : lap - laps[i - 1]; + }); + + const minDelta = Math.min(...deltas); + const maxDelta = Math.max(...deltas); + + // Render in reverse (newest first) + [...laps].reverse().forEach((lap, reverseIdx) => { + const idx = laps.length - 1 - reverseIdx; + const delta = deltas[idx]; + const time = formatTime(lap); + const deltaTime = formatTime(delta); + + let deltaClass = ""; + if (laps.length > 1) { + if (delta === minDelta) deltaClass = "fastest"; + else if (delta === maxDelta) deltaClass = "slowest"; + } + + const item = document.createElement("div"); + item.className = "lap-item"; + item.innerHTML = ` + Lap ${idx + 1} + +${deltaTime.mins}:${deltaTime.secs}.${deltaTime.cents} + ${time.mins}:${time.secs}.${time.cents} + `; + lapList.appendChild(item); + }); + } + + function tick(timestamp) { + if (!lastTimestamp) lastTimestamp = timestamp; + + const delta = timestamp - lastTimestamp; + lastTimestamp = timestamp; + + elapsedMs += delta; + updateDisplay(); + + if (isRunning) { + animationFrame = requestAnimationFrame(tick); + } + } + + function start() { + isRunning = true; + lastTimestamp = null; + animationFrame = requestAnimationFrame(tick); + updateButtons(); + setStateValue("running", true); + } + + function pause() { + isRunning = false; + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + updateButtons(); + setStateValue("running", false); + setStateValue("elapsed", elapsedMs); + } + + function recordLap() { + laps.push(elapsedMs); + renderLaps(); + setStateValue("laps", laps); + const t = formatTime(elapsedMs); + setTriggerValue("lap", { + number: laps.length, + time: elapsedMs, + formatted: `${t.mins}:${t.secs}.${t.cents}` + }); + } + + function reset() { + elapsedMs = 0; + laps = []; + updateDisplay(); + renderLaps(); + updateButtons(); + setStateValue("laps", []); + setStateValue("elapsed", 0); + setStateValue("running", false); + setTriggerValue("reset", true); + } + + // Event listeners + startBtn.addEventListener("click", () => { + if (isRunning) pause(); + else start(); + }); + + lapBtn.addEventListener("click", recordLap); + resetBtn.addEventListener("click", reset); + + // Initialize display + updateDisplay(); + updateButtons(); + renderLaps(); + + // Resume if was running + if (isRunning) { + lastTimestamp = null; + animationFrame = requestAnimationFrame(tick); + } + + return () => { + if (animationFrame) cancelAnimationFrame(animationFrame); + }; + } + """ +) + +# Render the component +result = stopwatch( + key="stopwatch", + on_lap_change=lambda: None, + on_reset_change=lambda: None, + on_running_change=lambda: None, + on_elapsed_change=lambda: None, + on_laps_change=lambda: None, + default={"elapsed": 0, "running": False, "laps": []}, +) + +# Display state info +col1, col2 = st.columns(2) +with col1: + st.metric("Status", "Running" if result.running else "Paused") + elapsed_sec = (result.elapsed or 0) / 1000 + st.metric("Elapsed", f"{elapsed_sec:.1f}s") +with col2: + st.subheader("Lap Records (Python)") + for i, lap_ms in enumerate(result.laps[-5:]): + mins, secs = divmod(lap_ms / 1000, 60) + st.write(f"**Lap {i+1}**: {int(mins):02d}:{secs:05.2f}") +``` + +## Best practices + +### When to use state values + +- Form inputs: Current values of text fields, dropdowns, checkboxes. +- Component configuration: Settings that affect how the component behaves. +- Selection state: Currently selected items in lists or tables. +- View state: Current tab, page, or mode in multi-view components. + +### When to use trigger values + +- User actions: Button clicks, form submissions, menu selections. +- Events: File uploads, drag-and-drop operations, keyboard shortcuts. +- Notifications: Status changes, error conditions, completion events. +- Navigation: Page changes, modal opens/closes. + +### Callback registration + +Both state and trigger values require callback registration using the `on__change` pattern. This ensures the component's result object consistently contains all of its state and trigger values, including on the first run. The following example mounts a component with callbacks for the following keys: + +- `"user_input"` state key +- `"selected_items"` state key +- `"button_click"` trigger key +- `"form_submit"` trigger key + +```python +result = my_component( + # State callbacks - called when state changes + on_user_input_change=handle_input_change, + on_selected_items_change=handle_selection_change, + + # Trigger callbacks - called when events fire + on_button_click_change=handle_button_click, + on_form_submit_change=handle_form_submit +) +``` + +### Default values + +Use the `default` parameter to set initial state values. If no default is provided, the state key will be set to `None`. Trigger values default (and revert after events) to `None`. The following example mounts a component with default values for the following keys: + +- `"user_input"` state key with an empty string. +- `"selected_items"` state key with an empty list. +- `"current_tab"` state key with `0`. +- `"button_click"` trigger key with `None` (Streamlit automatic default). + +```python +result = my_component( + default={ + "user_input": "", + "selected_items": [], + "current_tab": 0 + }, + on_user_input_change=handle_input, + on_selected_items_change=handle_selection, + on_current_tab_change=handle_tab_change, + on_button_click_change=handle_button_click +) +``` + +## What's next? + +Now that you understand state and trigger values: + +- Learn about [Theming and styling](/develop/concepts/custom-components/components-v2/theming) to make your components look great. +- Explore [Package-based components](/develop/concepts/custom-components/components-v2/package-based) for complex projects with TypeScript. +- Check out the [JavaScript API reference](/develop/api-reference/custom-components/component-v2-lib) for complete frontend documentation. diff --git a/content/develop/concepts/custom-components/components-v2/theming.md b/content/develop/concepts/custom-components/components-v2/theming.md new file mode 100644 index 000000000..ee13e98bb --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -0,0 +1,562 @@ +--- +title: Component theming and styling +slug: /develop/concepts/custom-components/components-v2/theming +description: Learn how to style Custom Components v2 with Streamlit's theme integration, CSS custom properties, and responsive design patterns. +keywords: custom components v2, theming, CSS custom properties, styling, theme integration, responsive design, dark mode, light mode, component styling +--- + +# Component theming and styling + +Custom components v2 provides seamless integration with Streamlit's theming system, allowing your components to automatically adapt to different themes, including dark and light modes. This integration is achieved through [CSS Custom Properties](https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Cascading_variables/Using_custom_properties) that expose Streamlit's theme values directly to your component styles. + +## Accessing theme values + +Streamlit automatically injects CSS Custom Properties into a wrapper element around your component instance. These properties are derived from the current Streamlit theme and are prefixed with `--st-` for easy identification. These values respect the viewer's theme selection, so you don't need separate logic to handle light and dark modes. + +## Using CSS custom properties + +Reference Streamlit theme values in your component styles using the [`var()`](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Values/var) CSS function. If your component has an HTML element with the class `my-component`, the following CSS will use the following theme values: + +- `--st-text-color` for `theme.textColor` +- `--st-background-color` for `theme.backgroundColor` +- `--st-border-color` for `theme.borderColor` +- `--st-font` for `theme.font` + +```css +.my-component { + color: var(--st-text-color); + background: var(--st-background-color); + border: 1px solid var(--st-border-color); + font-family: var(--st-font); +} +``` + +If your component is mounted in the sidebar, these values will correctly inherit from `theme.sidebar`. + +## Convert theme configuration option names to CSS custom property names + +In general, for any theme configuration option, use the CSS custom property `--st-` to reference the value. `` is the name of the option in the theme configuration in dash-case, also known as kebab-case. + +For example, to reference the primary color (`theme.primaryColor`), use `--st-primary-color`. To reference the background color (`theme.backgroundColor`), use `--st-background-color`. For a desciption of all theme configuration options, see the [`config.toml` API reference](/develop/api-reference/configuration/config.toml#theme). + +If a theme value is not configured, the CSS Custom Properties will have a valid value inherited from the current base theme. + +### Computed CSS Custom Properties + +There are a few computed CSS Custom Properties that don't come directly from a theme configuration option. The following CSS Custom Properties are computed: + +| CSS Custom Property | Used for | +| :------------------------- | :------------------------------------------------------- | +| `--st-heading-color` | Heading font color (placeholder); same as text color | +| `--st-border-color-light` | Lighter border color for stale or deactivated elements | +| `--st-widget-border-color` | Widget borders (when `theme.showWidgetBorder` is `true`) | + +### CSS Custom Property arrays + +Some theme properties are arrays. These are exposed as comma-separated strings. You can parse these in JavaScript if needed for dynamic styling. + +| CSS Custom Property | Used for | +| :------------------------------ | :----------------------------- | +| `--st-heading-font-sizes` | `theme.headingFontSizes` | +| `--st-heading-font-weights` | `theme.headingFontWeights` | +| `--st-chart-categorical-colors` | `theme.chartCategoricalColors` | +| `--st-chart-sequential-colors` | `theme.chartSequentialColors` | +| `--st-chart-diverging-colors` | `theme.chartDivergingColors` | + +Heading font sizes and weights are also available as individual CSS Custom Properties for each heading level (1–6), so you don't need to parse the arrays when styling specific headings: + +| CSS Custom Property | Used for | +| :--------------------------- | :---------------------------- | +| `--st-heading-font-size-1` | `theme.headingFontSizes[0]` | +| `--st-heading-font-size-2` | `theme.headingFontSizes[1]` | +| `--st-heading-font-size-3` | `theme.headingFontSizes[2]` | +| `--st-heading-font-size-4` | `theme.headingFontSizes[3]` | +| `--st-heading-font-size-5` | `theme.headingFontSizes[4]` | +| `--st-heading-font-size-6` | `theme.headingFontSizes[5]` | +| `--st-heading-font-weight-1` | `theme.headingFontWeights[0]` | +| `--st-heading-font-weight-2` | `theme.headingFontWeights[1]` | +| `--st-heading-font-weight-3` | `theme.headingFontWeights[2]` | +| `--st-heading-font-weight-4` | `theme.headingFontWeights[3]` | +| `--st-heading-font-weight-5` | `theme.headingFontWeights[4]` | +| `--st-heading-font-weight-6` | `theme.headingFontWeights[5]` | + +### Directly mapped CSS custom properties + +The rest of the CSS Custom Properties are directly mapped to theme configuration options and are usable without parsing or modification: + +| CSS Custom Property | `config.toml` theme option | +| :--------------------------------------- | :------------------------------------- | +| `--st-primary-color` | `theme.primaryColor` | +| `--st-background-color` | `theme.backgroundColor` | +| `--st-secondary-background-color` | `theme.secondaryBackgroundColor` | +| `--st-text-color` | `theme.textColor` | +| `--st-link-color` | `theme.linkColor` | +| `--st-link-underline` | `theme.linkUnderline` | +| `--st-heading-font` | `theme.headingFont` | +| `--st-code-font` | `theme.codeFont` | +| `--st-base-radius` | `theme.baseRadius` | +| `--st-button-radius` | `theme.buttonRadius` | +| `--st-base-font-size` | `theme.baseFontSize` | +| `--st-base-font-weight` | `theme.baseFontWeight` | +| `--st-code-font-weight` | `theme.codeFontWeight` | +| `--st-code-font-size` | `theme.codeFontSize` | +| `--st-code-text-color` | `theme.codeTextColor` | +| `--st-border-color` | `theme.borderColor` | +| `--st-dataframe-border-color` | `theme.dataframeBorderColor` | +| `--st-dataframe-header-background-color` | `theme.dataframeHeaderBackgroundColor` | +| `--st-code-background-color` | `theme.codeBackgroundColor` | +| `--st-font` | `theme.font` | +| `--st-red-color` | `theme.redColor` | +| `--st-orange-color` | `theme.orangeColor` | +| `--st-yellow-color` | `theme.yellowColor` | +| `--st-blue-color` | `theme.blueColor` | +| `--st-green-color` | `theme.greenColor` | +| `--st-violet-color` | `theme.violetColor` | +| `--st-gray-color` | `theme.grayColor` | +| `--st-red-background-color` | `theme.redBackgroundColor` | +| `--st-orange-background-color` | `theme.orangeBackgroundColor` | +| `--st-yellow-background-color` | `theme.yellowBackgroundColor` | +| `--st-blue-background-color` | `theme.blueBackgroundColor` | +| `--st-green-background-color` | `theme.greenBackgroundColor` | +| `--st-violet-background-color` | `theme.violetBackgroundColor` | +| `--st-gray-background-color` | `theme.grayBackgroundColor` | +| `--st-red-text-color` | `theme.redTextColor` | +| `--st-orange-text-color` | `theme.orangeTextColor` | +| `--st-yellow-text-color` | `theme.yellowTextColor` | +| `--st-blue-text-color` | `theme.blueTextColor` | +| `--st-green-text-color` | `theme.greenTextColor` | +| `--st-violet-text-color` | `theme.violetTextColor` | +| `--st-gray-text-color` | `theme.grayTextColor` | +| `--st-metric-value-font-size` | `theme.metricValueFontSize` | +| `--st-metric-value-font-weight` | `theme.metricValueFontWeight` | + +## Practical theming examples + +### Basic themed component + +Here's a simple component that uses Streamlit's theming. Instead of using pixels for spacing, the component uses rem values. This ensures that the component will adjust to different font sizes. The font family and size are set on the parent container so they can be inherited by other elements. Execeptions like headers are styled in later lines. In genral, set colors, borders, border radii, and fonts from CSS Custom Properties. + +```python +import streamlit as st + +themed_card = st.components.v2.component( + name="themed_card", + html=""" +
+

Themed Card

+

+ This card automatically adapts to Streamlit's current theme. +

+ +
+ """, + css=""" + .card { + background: var(--st-secondary-background-color); + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + padding: 1.25rem; + margin: 0.625rem 0; + font-family: var(--st-font); + font-family: var(--st-font); + font-size: var(--st-base-font-size); + } + + .card-title { + color: var(--st-heading-color); + font-family: var(--st-heading-font); + font-size: 1.2em; + margin: 0 0 0.625rem 0; + font-weight: 600; + } + + .card-content { + color: var(--st-text-color); + line-height: 1.5; + margin: 0 0 15px 0; + } + + .card-button { + background: var(--st-primary-color); + color: white; + border: none; + border-radius: var(--st-button-radius); + padding: 0.5rem 1rem; + cursor: pointer; + transition: opacity 0.2s; + } + + .card-button:hover { + opacity: 0.8; + } + """, + js=""" + export default function({ parentElement, setTriggerValue }) { + const cardButton = parentElement.querySelector('.card-button'); + cardButton.onclick = () => { + setTriggerValue('button_click', 'clicked'); + }; + } + """ +) + +result = themed_card(key="themed_example", on_button_click_change=lambda: None) +if result.button_click: + st.write("Card button clicked!") +``` + +### Status message component + +The following example demonstrates using Streamlit's basic color palette to set semantic colors. This is a component that creates color-coded alert banners: + +```python +import streamlit as st + +status_component = st.components.v2.component( + name="status_message", + html=""" +
+ + +
+ """, + css=""" + .status { + display: flex; + align-items: center; + padding: 0.75rem 1rem; + margin: 0.5rem 0; + border-radius: var(--st-base-radius); + border-left: 0.25rem solid; + font-family: var(--st-font); + } + + .status.success { + background: var(--st-green-background-color); + border-left-color: var(--st-green-color); + color: var(--st-text-color); + } + + .status.warning { + background: var(--st-yellow-background-color); + border-left-color: var(--st-yellow-color); + color: var(--st-text-color); + } + + .status.error { + background: var(--st-red-background-color); + border-left-color: var(--st-red-color); + color: var(--st-text-color); + } + + .status.info { + background: var(--st-blue-background-color); + border-left-color: var(--st-blue-color); + color: var(--st-text-color); + } + + .icon { + margin-right: 0.625rem; + font-size: 1rem; + } + + .message { + flex: 1; + font-size: var(--st-base-font-size); + } + """, + js=""" + export default function({ parentElement, data }) { + const container = parentElement.querySelector('#status-container'); + const icon = parentElement.querySelector('#icon'); + const message = parentElement.querySelector('#message'); + + // Set the status type class + container.className = `status ${data.type}`; + + // Set the icon based on type + const icons = { + success: '✅', + warning: '⚠️', + error: '❌', + info: 'ℹ️' + }; + + icon.textContent = icons[data.type] || '•'; + message.textContent = data.message; + } + """ +) + +# Mount the component four times with different status types +status_component( + data={"type": "success", "message": "Operation completed successfully"}, + key="status_success" +) + +status_component( + data={"type": "warning", "message": "Please review your settings"}, + key="status_warning" +) + +status_component( + data={"type": "error", "message": "An error occurred during processing"}, + key="status_error" +) + +status_component( + data={"type": "info", "message": "Additional information available"}, + key="status_info" +) +``` + +### Data table component + +You can use CSS Custom Properties to style a data table to match Streamlit's dataframe styling. + +```python +import streamlit as st + +data_table = st.components.v2.component( + name="custom_table", + html=""" +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
NameValueStatus
Item 1100Active
Item 2250Pending
Item 375Inactive
+
+ """, + css=""" + .table-container { + font-family: var(--st-font); + overflow-x: auto; + } + + .data-table { + width: 100%; + border-collapse: collapse; + background: var(--st-background-color); + border: 1px solid var(--st-dataframe-border-color); + border-radius: var(--st-base-radius); + overflow: hidden; + } + + .data-table th { + background: var(--st-dataframe-header-background-color); + color: var(--st-text-color); + font-weight: 600; + padding: 0.75rem 1rem; + text-align: left; + border-bottom: 1px solid var(--st-dataframe-border-color); + font-size: var(--st-base-font-size); + } + + .data-table td { + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--st-dataframe-border-color); + color: var(--st-text-color); + font-size: var(--st-base-font-size); + } + + .data-table tr:last-child td { + border-bottom: none; + } + + .data-table tr:hover { + background: var(--st-secondary-background-color); + } + + .status-badge { + padding: 0.25rem 0.5rem; + border-radius: calc(var(--st-base-radius) / 2); + font-size: 0.75rem; + font-weight: 500; + } + + .status-badge.success { + background: var(--st-green-background-color); + color: var(--st-green-color); + } + + .status-badge.warning { + background: var(--st-yellow-background-color); + color: var(--st-yellow-color); + } + + .status-badge.error { + background: var(--st-red-background-color); + color: var(--st-red-color); + } + """ +) + +result = data_table(key="table_example") +``` + +## Style isolation + +Custom components v2 provides style isolation options to control how your component styles interact with the rest of the page. + +### Isolated styles (default) + +By default, Streamlit sets `isolate_styles=True` when mounting a component, which wraps the instance in a Shadow DOM: + +```python +my_component = st.components.v2.component( + name="my_component", + html="
Isolated content
", + css=".my-style { color: red; }", +) + +# Styles are isolated (default behavior) +my_component(isolate_styles=True) +``` + +Benefits of isolation: + +- Component styles won't leak to the rest of the page. +- Page styles won't interfere with your component. +- Safer for third-party components. + +### Non-isolated styles + +If you want your component's style to affect the rest of the page, you can set `isolate_styles=False` when mounting. This is uncommon. + +```python +# Styles can affect the page +my_component(isolate_styles=False) +``` + +## Responsive design + +Create components that work well across different screen sizes. This makes your component more accessible and compatible with the Streamlit layout system. The following example uses `@media (max-width: 768px)` to create a responsive grid layout that adapts when the screen width is less than 768px. + +```python +import streamlit as st + +responsive_component = st.components.v2.component( + name="responsive_layout", + html=""" +
+
Item 1
+
Item 2
+
Item 3
+
Item 4
+
+ """, + css=""" + .responsive-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + padding: 1rem; + font-family: var(--st-font); + } + + .grid-item { + background: var(--st-secondary-background-color); + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + padding: 1.25rem; + text-align: center; + color: var(--st-text-color); + transition: transform 0.2s; + } + + .grid-item:hover { + transform: translateY(-2px); + box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1); + } + + /* Mobile-specific styles */ + @media (max-width: 768px) { + .responsive-grid { + grid-template-columns: 1fr; + gap: 0.75rem; + padding: 0.75rem; + } + + .grid-item { + padding: 1rem; + } + } + """ +) + +responsive_component(key="responsive_example") +``` + +## Best practices + +### Always use theme variables + +Instead of hardcoding colors, always use Streamlit's theme variables: + +```css +/* Don't do this */ +.my-component { + color: #262730; + background: #ffffff; +} + +/* Do this instead */ +.my-component { + color: var(--st-text-color); + background: var(--st-background-color); +} +``` + +### Test in different themes + +Always test your components in both light and dark base themes. Preferably, test your component with a custom theme as well, especially using different font sizes. + +### Use semantic color names + +Choose colors from the basic color palette based on their semantic meaning. Each color in the basic color palette has a text and background variation, in addition to its base color. + +```css +/* Good - semantic usage */ +.error-message { + color: var(--st-red-text-color); + background: var(--st-red-background-color); +} + +.success-indicator { + color: var(--st-green-color); +} +``` + +### Respect accessibility + +Streamlit's theme colors are designed with accessibility in mind. Maintain proper contrast ratios when creating custom color combinations. + +## What's next? + +Now that you understand theming and styling: + +- Explore [Package-based components](/develop/concepts/custom-components/components-v2/package-based) for advanced development workflows. +- Learn about [State vs triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) for interactive components. +- Check out the [Quickstart examples](/develop/concepts/custom-components/components-v2/examples) for more examples. diff --git a/content/develop/concepts/custom-components/overview.md b/content/develop/concepts/custom-components/overview.md new file mode 100644 index 000000000..4c519344c --- /dev/null +++ b/content/develop/concepts/custom-components/overview.md @@ -0,0 +1,53 @@ +--- +title: Overview of custom components +slug: /develop/concepts/custom-components/overview +description: Understand what Streamlit custom components are, when to use them, and compare the v1 and v2 approaches for building interactive extensions. +keywords: custom components overview, component comparison, v1 vs v2, component capabilities, when to use components, component architecture +--- + +# Overview of custom components + +Custom components are like plugins for Streamlit that unlock capabilities beyond the built-in features. They let you integrate any web technology directly into your Streamlit app. You can create single-use custom components in your app, or package a custom component to share. + +Custom components can help you in the following situations: + +- **Built-in widgets don't meet your needs** - You need functionality that Streamlit's standard widgets can't provide. +- **You want to integrate existing web tools** - You have JavaScript libraries or web components you want to use. +- **You need complex interactions** - Your use case requires bidirectional communication or complex state management. +- **You're building reusable functionality** - You want to package and share functionality across multiple apps or with the community. + +## Components v2 (recommended) + +Custom components v2 is the modern, recommended approach for building custom components in Streamlit. It represents a complete reimagining of how components work. It's designed to unlock new capabilities and dramatically simplify development. + +Custom components v2 include the following benefits: + +- **No iframe isolation** - Components are part of the Streamlit page, not isolated sandboxes. +- **Multiple callback support** - You can pass multiple callbacks to a component for rich interactions. +- **Stateful and event-based values** - Components have both state and event-based trigger values. +- **Rich data exchange** - Components automatically handle JSON and dataframe (Apache Arrow) serialization. +- **Simpler development and rapid prototyping** - You can provide HTML, CSS, and JavaScript directly from Python or build a package with TypeScript. +- **Bidirectional communication** - Convenient utilities make bidirectional communication easy. +- **Seamless Theme Integration** - Components automatically inherit Streamlit's theme through CSS custom properties. + +## Components v1 (legacy) + +Components v1 is the original framework that has been stable and widely used since 2020. While components v2 is now the recommended approach, components v1 remains supported for existing components. + +V1 components have the following key differences from v2 components: + +- **Iframe isolation** - Components run in isolated iframes for security. +- **Primarily unidirectional communication** - The API is less optimized for bidirectional communication. +- **Mature ecosystem** - Many existing components and templates use the v1 architecture. + +## Comparing components v1 and v2 + +| Feature | Components v2 **Recommended** | Components v1 | +| -------------------- | ------------------------------------------ | ------------------------ | +| **Communication** | Full bidirectional with multiple callbacks | Primarily unidirectional | +| **Isolation** | Integrated with page | Iframe-based | +| **Data exchange** | Rich formats (JSON, Arrow, bytes) | Basic JSON | +| **Development** | Inline or package-based | Template-based | +| **State management** | Full state and trigger support | Limited | +| **Prototyping** | Immediate with inline approach | Requires setup | +| **Best for** | New projects and modern features | Existing components | diff --git a/content/develop/concepts/custom-components/publish-component.md b/content/develop/concepts/custom-components/publish.md similarity index 100% rename from content/develop/concepts/custom-components/publish-component.md rename to content/develop/concepts/custom-components/publish.md diff --git a/content/menu.md b/content/menu.md index f8c39ae9e..1611c511a 100644 --- a/content/menu.md +++ b/content/menu.md @@ -100,14 +100,50 @@ site_menu: url: /develop/concepts/connections/security-reminders - category: Develop / Concepts / Custom components url: /develop/concepts/custom-components - - category: Develop / Concepts / Custom components / Intro to custom components - url: /develop/concepts/custom-components/intro - - category: Develop / Concepts / Custom components / Create a Component - url: /develop/concepts/custom-components/create - - category: Develop / Concepts / Custom components / Publish a Component + - category: Develop / Concepts / Custom components / Overview + url: /develop/concepts/custom-components/overview + - category: Develop / Concepts / Custom components / Components v2 + url: /develop/concepts/custom-components/components-v2 + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples + url: /develop/concepts/custom-components/components-v2/examples + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Hello world + url: /develop/concepts/custom-components/components-v2/examples/hello-world + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Rich data + url: /develop/concepts/custom-components/components-v2/examples/rich-data + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Simple button + url: /develop/concepts/custom-components/components-v2/examples/simple-button + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Simple checkbox + url: /develop/concepts/custom-components/components-v2/examples/simple-checkbox + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Interactive counter + url: /develop/concepts/custom-components/components-v2/examples/interactive-counter + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Text input + url: /develop/concepts/custom-components/components-v2/examples/text-input + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Danger button + url: /develop/concepts/custom-components/components-v2/examples/danger-button + - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples / Radial menu + url: /develop/concepts/custom-components/components-v2/examples/radial-menu + - category: Develop / Concepts / Custom components / Components v2 / Registration + url: /develop/concepts/custom-components/components-v2/register + - category: Develop / Concepts / Custom components / Components v2 / Mounting + url: /develop/concepts/custom-components/components-v2/mount + - category: Develop / Concepts / Custom components / Components v2 / State vs trigger values + url: /develop/concepts/custom-components/components-v2/state-and-triggers + - category: Develop / Concepts / Custom components / Components v2 / Bidirectional communication + url: /develop/concepts/custom-components/components-v2/communicate + - category: Develop / Concepts / Custom components / Components v2 / Theming and styling + url: /develop/concepts/custom-components/components-v2/theming + - category: Develop / Concepts / Custom components / Components v2 / Package-based components + url: /develop/concepts/custom-components/components-v2/package-based + - category: Develop / Concepts / Custom components / Components v1 + url: /develop/concepts/custom-components/components-v1 + - category: Develop / Concepts / Custom components / Components v1 / Intro to v1 components + url: /develop/concepts/custom-components/components-v1/intro + - category: Develop / Concepts / Custom components / Components v1 / Create a component + url: /develop/concepts/custom-components/components-v1/create + - category: Develop / Concepts / Custom components / Components v1 / Limitations + url: /develop/concepts/custom-components/components-v1/limitations + - category: Develop / Concepts / Custom components / Publish a component url: /develop/concepts/custom-components/publish - - category: Develop / Concepts / Custom components / Limitations - url: /develop/concepts/custom-components/limitations - category: Develop / Concepts / Custom components / Component gallery url: https://streamlit.io/components - category: Develop / Concepts / Configuration and theming diff --git a/public/_redirects b/public/_redirects index 2e523329b..dbfb2c49b 100644 --- a/public/_redirects +++ b/public/_redirects @@ -43,14 +43,14 @@ /en/stable/caching.html /develop/concepts/architecture/caching /en/stable/changelog.html /develop/quick-reference/release-notes /en/stable/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/stable/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/stable/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/stable/getting_started.html /get-started /en/stable/index.html / /en/stable/installation.html /get-started/installation /en/stable/main_concepts.html /get-started/fundamentals/main-concepts /en/stable/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/stable/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/stable/streamlit_components.html /develop/concepts/custom-components/create +/en/stable/streamlit_components.html /develop/concepts/custom-components/v1/create /en/stable/streamlit_components_faq.html /knowledge-base/components /en/stable/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/stable/streamlit_faq.html /knowledge-base @@ -99,13 +99,13 @@ /en/0.63.0/caching.html /develop/concepts/architecture/caching /en/0.63.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.63.0/cli.html /get-started -/en/0.63.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.63.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.63.0/getting_started.html /get-started /en/0.63.0/index.html / /en/0.63.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.63.0/pre_release_features.html /get-started /en/0.63.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.63.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.63.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.63.0/streamlit_components_faq.html /knowledge-base/components /en/0.63.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues /en/0.63.0/troubleshooting/clean-install.html /get-started/installation @@ -122,13 +122,13 @@ /en/0.64.0/caching.html /develop/concepts/architecture/caching /en/0.64.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.64.0/cli.html /get-started -/en/0.64.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.64.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.64.0/getting_started.html /get-started /en/0.64.0/index.html / /en/0.64.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.64.0/pre_release_features.html /get-started /en/0.64.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.64.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.64.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.64.0/streamlit_components_faq.html /knowledge-base/components /en/0.64.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues /en/0.64.0/troubleshooting/clean-install.html /get-started/installation @@ -145,13 +145,13 @@ /en/0.65.0/caching.html /develop/concepts/architecture/caching /en/0.65.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.65.0/cli.html /get-started -/en/0.65.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.65.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.65.0/getting_started.html /get-started /en/0.65.0/index.html / /en/0.65.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.65.0/pre_release_features.html /get-started /en/0.65.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.65.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.65.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.65.0/streamlit_faq.html /knowledge-base /en/0.65.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues /en/0.65.0/troubleshooting/clean-install.html /get-started/installation @@ -168,13 +168,13 @@ /en/0.66.0/caching.html /develop/concepts/architecture/caching /en/0.66.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.66.0/cli.html /get-started -/en/0.66.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.66.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.66.0/getting_started.html /get-started /en/0.66.0/index.html / /en/0.66.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.66.0/pre_release_features.html /get-started /en/0.66.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.66.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.66.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.66.0/streamlit_faq.html /knowledge-base /en/0.66.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues /en/0.66.0/troubleshooting/clean-install.html /get-started/installation @@ -191,13 +191,13 @@ /en/0.67.0/caching.html /develop/concepts/architecture/caching /en/0.67.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.67.0/cli.html /get-started -/en/0.67.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.67.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.67.0/getting_started.html /get-started /en/0.67.0/index.html / /en/0.67.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.67.0/pre_release_features.html /get-started /en/0.67.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.67.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.67.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.67.0/streamlit_faq.html /knowledge-base /en/0.67.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues /en/0.67.0/troubleshooting/clean-install.html /get-started/installation @@ -214,14 +214,14 @@ /en/0.68.0/caching.html /develop/concepts/architecture/caching /en/0.68.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.68.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.68.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.68.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.68.0/getting_started.html /get-started /en/0.68.0/getting_started.md /get-started /en/0.68.0/index.html / /en/0.68.0/installation.html /get-started/installation /en/0.68.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.68.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.68.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.68.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.68.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.68.0/streamlit_faq.html /knowledge-base /en/0.68.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -237,14 +237,14 @@ /en/0.69.0/caching.html /develop/concepts/architecture/caching /en/0.69.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.69.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.69.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.69.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.69.0/getting_started.html /get-started /en/0.69.0/getting_started.md /get-started /en/0.69.0/index.html / /en/0.69.0/installation.html /get-started/installation /en/0.69.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.69.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.69.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.69.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.69.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.69.0/streamlit_faq.html /knowledge-base /en/0.69.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -260,14 +260,14 @@ /en/0.70.0/caching.html /develop/concepts/architecture/caching /en/0.70.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.70.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.70.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.70.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.70.0/getting_started.html /get-started /en/0.70.0/getting_started.md /get-started /en/0.70.0/index.html / /en/0.70.0/installation.html /get-started/installation /en/0.70.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.70.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.70.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.70.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.70.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.70.0/streamlit_faq.html /knowledge-base /en/0.70.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -283,14 +283,14 @@ /en/0.71.0/caching.html /develop/concepts/architecture/caching /en/0.71.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.71.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.71.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.71.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.71.0/getting_started.html /get-started /en/0.71.0/getting_started.md /get-started /en/0.71.0/index.html / /en/0.71.0/installation.html /get-started/installation /en/0.71.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.71.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.71.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.71.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.71.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.71.0/streamlit_faq.html /knowledge-base /en/0.71.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -306,14 +306,14 @@ /en/0.72.0/caching.html /develop/concepts/architecture/caching /en/0.72.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.72.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.72.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.72.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.72.0/getting_started.html /get-started /en/0.72.0/getting_started.md /get-started /en/0.72.0/index.html / /en/0.72.0/installation.html /get-started/installation /en/0.72.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.72.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.72.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.72.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.72.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.72.0/streamlit_faq.html /knowledge-base /en/0.72.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -329,14 +329,14 @@ /en/0.73.0/caching.html /develop/concepts/architecture/caching /en/0.73.0/changelog.html /develop/quick-reference/release-notes/2020 /en/0.73.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.73.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.73.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.73.0/getting_started.html /get-started /en/0.73.0/getting_started.md /get-started /en/0.73.0/index.html / /en/0.73.0/installation.html /get-started/installation /en/0.73.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.73.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.73.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.73.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.73.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.73.0/streamlit_faq.html /knowledge-base /en/0.73.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -352,14 +352,14 @@ /en/0.74.0/caching.html /develop/concepts/architecture/caching /en/0.74.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.74.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.74.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.74.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.74.0/getting_started.html /get-started /en/0.74.0/getting_started.md /get-started /en/0.74.0/index.html / /en/0.74.0/installation.html /get-started/installation /en/0.74.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.74.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.74.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.74.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.74.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.74.0/streamlit_faq.html /knowledge-base /en/0.74.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -375,14 +375,14 @@ /en/0.75.0/caching.html /develop/concepts/architecture/caching /en/0.75.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.75.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.75.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.75.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.75.0/getting_started.html /get-started /en/0.75.0/getting_started.md /get-started /en/0.75.0/index.html / /en/0.75.0/installation.html /get-started/installation /en/0.75.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.75.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.75.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.75.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.75.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.75.0/streamlit_faq.html /knowledge-base /en/0.75.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -398,14 +398,14 @@ /en/0.76.0/caching.html /develop/concepts/architecture/caching /en/0.76.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.76.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.76.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.76.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.76.0/getting_started.html /get-started /en/0.76.0/getting_started.md /get-started /en/0.76.0/index.html / /en/0.76.0/installation.html /get-started/installation /en/0.76.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.76.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.76.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.76.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.76.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.76.0/streamlit_faq.html /knowledge-base /en/0.76.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -421,14 +421,14 @@ /en/0.77.0/caching.html /develop/concepts/architecture/caching /en/0.77.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.77.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.77.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.77.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.77.0/getting_started.html /get-started /en/0.77.0/getting_started.md /get-started /en/0.77.0/index.html / /en/0.77.0/installation.html /get-started/installation /en/0.77.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.77.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.77.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.77.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.77.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.77.0/streamlit_faq.html /knowledge-base /en/0.77.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -444,14 +444,14 @@ /en/0.78.0/caching.html /develop/concepts/architecture/caching /en/0.78.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.78.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.78.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.78.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.78.0/getting_started.html /get-started /en/0.78.0/getting_started.md /get-started /en/0.78.0/index.html / /en/0.78.0/installation.html /get-started/installation /en/0.78.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.78.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.78.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.78.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.78.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.78.0/streamlit_faq.html /knowledge-base /en/0.78.0/troubleshooting/caching_issues.html /knowledge-base/using-streamlit/caching-issues @@ -466,13 +466,13 @@ /en/0.79.0/caching.html /develop/concepts/architecture/caching /en/0.79.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.79.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.79.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.79.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.79.0/getting_started.html /get-started /en/0.79.0/index.html / /en/0.79.0/installation.html /get-started/installation /en/0.79.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.79.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.79.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.79.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.79.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.79.0/streamlit_faq.html /knowledge-base /en/0.79.0/theme_options.html /develop/concepts/configuration/theming @@ -488,13 +488,13 @@ /en/0.80.0/caching.html /develop/concepts/architecture/caching /en/0.80.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.80.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.80.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.80.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.80.0/getting_started.html /get-started /en/0.80.0/index.html / /en/0.80.0/installation.html /get-started/installation /en/0.80.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.80.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.80.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.80.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.80.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.80.0/streamlit_faq.html /knowledge-base /en/0.80.0/theme_options.html /develop/concepts/configuration/theming @@ -510,13 +510,13 @@ /en/0.81.0/caching.html /develop/concepts/architecture/caching /en/0.81.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.81.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.81.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.81.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.81.0/getting_started.html /get-started /en/0.81.0/index.html / /en/0.81.0/installation.html /get-started/installation /en/0.81.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.81.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.81.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.81.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.81.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.81.0/streamlit_faq.html /knowledge-base /en/0.81.0/theme_options.html /develop/concepts/configuration/theming @@ -532,13 +532,13 @@ /en/0.81.1/caching.html /develop/concepts/architecture/caching /en/0.81.1/changelog.html /develop/quick-reference/release-notes/2021 /en/0.81.1/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.81.1/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.81.1/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.81.1/getting_started.html /get-started /en/0.81.1/index.html / /en/0.81.1/installation.html /get-started/installation /en/0.81.1/main_concepts.html /get-started/fundamentals/main-concepts /en/0.81.1/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.81.1/streamlit_components.html /develop/concepts/custom-components/create +/en/0.81.1/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.81.1/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.81.1/streamlit_faq.html /knowledge-base /en/0.81.1/theme_options.html /develop/concepts/configuration/theming @@ -554,13 +554,13 @@ /en/0.82.0/caching.html /develop/concepts/architecture/caching /en/0.82.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.82.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.82.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.82.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.82.0/getting_started.html /get-started /en/0.82.0/index.html / /en/0.82.0/installation.html /get-started/installation /en/0.82.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.82.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.82.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.82.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.82.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.82.0/streamlit_faq.html /knowledge-base /en/0.82.0/theme_options.html /develop/concepts/configuration/theming @@ -576,13 +576,13 @@ /en/0.83.0/caching.html /develop/concepts/architecture/caching /en/0.83.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.83.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.83.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.83.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.83.0/getting_started.html /get-started /en/0.83.0/index.html / /en/0.83.0/installation.html /get-started/installation /en/0.83.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.83.0/publish_streamlit_components.html /develop/concepts/custom-components/publish -/en/0.83.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.83.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.83.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.83.0/streamlit_faq.html /knowledge-base /en/0.83.0/theme_options.html /develop/concepts/configuration/theming @@ -607,14 +607,14 @@ /en/0.84.0/caching.html /develop/concepts/architecture/caching /en/0.84.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.84.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.84.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.84.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.84.0/getting_started.html /get-started /en/0.84.0/index.html / /en/0.84.0/installation.html /get-started/installation /en/0.84.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.84.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.84.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.84.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.84.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.84.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.84.0/streamlit_faq.html /knowledge-base /en/0.84.0/theme_options.html /develop/concepts/configuration/theming @@ -639,14 +639,14 @@ /en/0.85.0/caching.html /develop/concepts/architecture/caching /en/0.85.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.85.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.85.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.85.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.85.0/getting_started.html /get-started /en/0.85.0/index.html / /en/0.85.0/installation.html /get-started/installation /en/0.85.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.85.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.85.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.85.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.85.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.85.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.85.0/streamlit_faq.html /knowledge-base /en/0.85.0/theme_options.html /develop/concepts/configuration/theming @@ -672,14 +672,14 @@ /en/0.86.0/caching.html /develop/concepts/architecture/caching /en/0.86.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.86.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.86.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.86.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.86.0/getting_started.html /get-started /en/0.86.0/index.html / /en/0.86.0/installation.html /get-started/installation /en/0.86.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.86.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.86.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.86.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.86.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.86.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.86.0/streamlit_faq.html /knowledge-base /en/0.86.0/theme_options.html /develop/concepts/configuration/theming @@ -705,14 +705,14 @@ /en/0.87.0/caching.html /develop/concepts/architecture/caching /en/0.87.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.87.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.87.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.87.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.87.0/getting_started.html /get-started /en/0.87.0/index.html / /en/0.87.0/installation.html /get-started/installation /en/0.87.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.87.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.87.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.87.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.87.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.87.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.87.0/streamlit_faq.html /knowledge-base /en/0.87.0/theme_options.html /develop/concepts/configuration/theming @@ -738,14 +738,14 @@ /en/0.88.0/caching.html /develop/concepts/architecture/caching /en/0.88.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.88.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.88.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.88.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.88.0/getting_started.html /get-started /en/0.88.0/index.html / /en/0.88.0/installation.html /get-started/installation /en/0.88.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.88.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.88.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.88.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.88.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.88.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.88.0/streamlit_faq.html /knowledge-base /en/0.88.0/theme_options.html /develop/concepts/configuration/theming @@ -771,14 +771,14 @@ /en/0.89.0/caching.html /develop/concepts/architecture/caching /en/0.89.0/changelog.html /develop/quick-reference/release-notes/2021 /en/0.89.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/0.89.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/0.89.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/0.89.0/getting_started.html /get-started /en/0.89.0/index.html / /en/0.89.0/installation.html /get-started/installation /en/0.89.0/main_concepts.html /get-started/fundamentals/main-concepts /en/0.89.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/0.89.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/0.89.0/streamlit_components.html /develop/concepts/custom-components/create +/en/0.89.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/0.89.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/0.89.0/streamlit_faq.html /knowledge-base /en/0.89.0/theme_options.html /develop/concepts/configuration/theming @@ -804,14 +804,14 @@ /en/1.0.0/caching.html /develop/concepts/architecture/caching /en/1.0.0/changelog.html /develop/quick-reference/release-notes/2021 /en/1.0.0/deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app -/en/1.0.0/develop_streamlit_components.html /develop/concepts/custom-components/intro +/en/1.0.0/develop_streamlit_components.html /develop/concepts/custom-components/v1/intro /en/1.0.0/getting_started.html /get-started /en/1.0.0/index.html / /en/1.0.0/installation.html /get-started/installation /en/1.0.0/main_concepts.html /get-started/fundamentals/main-concepts /en/1.0.0/publish_streamlit_components.html /develop/concepts/custom-components/publish /en/1.0.0/session_state_api.html /develop/api-reference/caching-and-state/st.session_state -/en/1.0.0/streamlit_components.html /develop/concepts/custom-components/create +/en/1.0.0/streamlit_components.html /develop/concepts/custom-components/v1/create /en/1.0.0/streamlit_configuration.html /develop/api-reference/configuration/config.toml /en/1.0.0/streamlit_faq.html /knowledge-base /en/1.0.0/theme_options.html /develop/concepts/configuration/theming @@ -833,7 +833,7 @@ /deploy_streamlit_app.html /deploy/streamlit-community-cloud/deploy-your-app /tutorial/create_a_data_explorer_app.html /get-started/tutorials/create-an-app /getting_started.html /get-started -/develop_streamlit_components.html /develop/concepts/custom-components/create +/develop_streamlit_components.html /develop/concepts/custom-components/v1/create /en/component_docs/ /develop/concepts/custom-components /api.html /develop/api-reference /en/stable/pre_release_features.html /develop/quick-reference/prerelease @@ -1066,8 +1066,8 @@ /library/advanced-features/app-testing/examples /develop/concepts/app-testing/examples /library/advanced-features/app-testing/cheat-sheet /develop/concepts/app-testing/cheat-sheet /library/components /develop/concepts/custom-components -/library/components/components-api /develop/concepts/custom-components/intro -/library/components/create /develop/concepts/custom-components/create +/library/components/components-api /develop/concepts/custom-components/v1/intro +/library/components/create /develop/concepts/custom-components/v1/create /library/components/publish /develop/concepts/custom-components/publish /library/changelog /develop/quick-reference/release-notes /library/cheatsheet /develop/quick-reference/cheat-sheet @@ -1159,6 +1159,10 @@ /develop/api-reference/custom-components/component-v2-lib-componentstate /develop/api-reference/custom-components/component-v2-lib-frontendstate /develop/api-reference/custom-components/component-v2-lib-component /develop/api-reference/custom-components/component-v2-lib-frontendrenderer /develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable /develop/api-reference/custom-components/st.components.v2.types.componentrenderer +/develop/concepts/custom-components/intro /develop/concepts/custom-components/v1/intro +/develop/concepts/custom-components/create /develop/concepts/custom-components/v1/create +/develop/concepts/custom-components/limitations /develop/concepts/custom-components/v1/limitations + # Deep links included in streamlit/streamlit source code /st.connections.snowflakeconnection-configuration /develop/api-reference/connections/st.connections.snowflakeconnection diff --git a/public/images/component-communication-cycle.svg b/public/images/component-communication-cycle.svg new file mode 100644 index 000000000..4fafd11b9 --- /dev/null +++ b/public/images/component-communication-cycle.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + setTriggerValue() + setStateValue() + + + + + + + + + + + + + + + + + + data + + + Python + + + JavaScript + + \ No newline at end of file