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..415440641 --- /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 complex components with modern frontend tooling, TypeScript, and external dependencies. + +
+ +
+ +## 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..b1f414fb0 --- /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 similar to that of native Streamlit widgets: setting a component's state from Session State. You don't strictly need feed a component's state from Session State back into itself to create a stateful component. This is just one pattern to achieve an experience that is similar to native Streamlit widgets. + +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 my_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 = my_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_text_input"]["value"] = "Hello World" + +if st.button("Clear"): + st.session_state["my_text_input"]["value"] = "" + +result = my_component_wrapper("Enter text", key="my_text_input") +``` + +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..6f8010e4d --- /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( + "rich_data", + 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 = False + +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 my_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 = my_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=handle_deletion, + 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`) +- Document-level event listeners +- 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..ccc31c2bc --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/danger-button.md @@ -0,0 +1,705 @@ +--- +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 + + + +```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}") +``` + + + +## File-based version + +```markup filename="my_component/component.html" + +``` + + + +```css filename="my_component/component.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 filename="my_component/component.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) { + 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 +from my_component import HTML, CSS, JS + +danger_button = st.components.v2.component( + name="hold_to_confirm", + html=HTML, + css=CSS, + js=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!"; +``` + +## Related documentation + +- [Component registration](/develop/concepts/custom-components/components-v2/register) +- [Component mounting](/develop/concepts/custom-components/components-v2/mount) +- [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate) +- [Theming and styling](/develop/concepts/custom-components/components-v2/theming) 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..a3b3cf697 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md @@ -0,0 +1,290 @@ +--- +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 and pasting, exapand the complete code below. However, for easier reading, the code is split into multiple files in the next section. + + + +```python filename="streamlit_app.py" +import streamlit as st + +# Interactive counter with both state and triggers +counter = 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(); + } + """, +) + +# Use with callbacks +result = counter( + 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!") +``` + +
+ +## File-based version + +```none filename="Directory structure" +my_app/ +├── streamlit_app.py +└── my_component/ + ├── __init__.py + ├── component.html + ├── component.css + └── component.js +``` + +```markup filename="my_component/component.html" +
+

Count: 0

+
+ + + +
+
+``` + +```css filename="my_component/component.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 filename="my_component/component.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(); +} +``` + +```python filename="my_component/__init__.py" +from pathlib import Path +import streamlit as st + +component_dir = Path(__file__).parent + + +@st.cache_data +def load_component_code(): + with open(component_dir / "component.css", "r") as f: + CSS = f.read() + with open(component_dir / "component.html", "r") as f: + HTML = f.read() + with open(component_dir / "component.js", "r") as f: + JS = f.read() + return HTML, CSS, JS + + +HTML, CSS, JS = load_component_code() + +interactive_counter = st.components.v2.component( + name="interactive_counter", + html=HTML, + css=CSS, + js=JS, +) +``` + +```python filename="streamlit_app.py" +from my_component import interactive_counter + +result = interactive_counter( + 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 contents of the HTML, CSS, and JavaScript files to the component registration function 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..d7acd6fd6 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md @@ -0,0 +1,376 @@ +--- +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`) +- Document-level event listeners +- Complex animations with CSS transitions + +## File-based version + +```none hideHeader +project_directory/ +├── radial_menu_component/ +│ ├── __init__.py +│ ├── menu.css +│ ├── menu.html +│ └── menu.js +└── streamlit_app.py +``` + + + +```python filename="radial_menu_component/__init__.py" +from pathlib import Path +import streamlit as st + +component_dir = Path(__file__).parent + + +@st.cache_data +def load_component_code(): + with open(component_dir / "menu.css", "r") as f: + CSS = f.read() + with open(component_dir / "menu.html", "r") as f: + HTML = f.read() + with open(component_dir / "menu.js", "r") as f: + JS = f.read() + return HTML, CSS, JS + + +HTML, CSS, JS = load_component_code() + +radial_menu = st.components.v2.component( + name="radial_menu", + html=HTML, + css=CSS, + js=JS, +) +``` + + + +```markup filename="radial_menu_component/menu.html" +
+ + + +
+``` + + + +```css filename="radial_menu_component/menu.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, 6)) - 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 */ +.menu-overlay::before { + content: ""; + position: fixed; + inset: -100vh -100vw; + background: var(--st-background-color); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: -1; +} + +.menu-overlay.open::before { + opacity: 0.7; + pointer-events: auto; +} + +/* 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 filename="radial_menu_component/menu.js" +export default function ({ parentElement, data, setStateValue }) { + const selector = parentElement.querySelector("#selector"); + const selectorIcon = parentElement.querySelector("#selector-icon"); + const overlay = parentElement.querySelector("#overlay"); + const ring = parentElement.querySelector("#ring"); + + let isOpen = false; + const options = data?.options || {}; + let currentSelection = data?.selection || Object.keys(options)[0]; + + // Create 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.style.setProperty("--total", Object.keys(options).length); + button.textContent = icon; + + button.addEventListener("click", () => { + 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, + ); + }); + } + + // Toggle menu open/closed + function toggleMenu() { + isOpen = !isOpen; + overlay.classList.toggle("open", isOpen); + ring.classList.toggle("open", isOpen); + } + + // Initialize display + updateDisplay(); + + // Selector click toggles menu + selector.addEventListener("click", toggleMenu); + + // Click outside closes menu + overlay.addEventListener("click", (e) => { + if (e.target === overlay) toggleMenu(); + }); + + // Document-level click to close + const handleOutsideClick = (e) => { + if (isOpen && !parentElement.contains(e.target)) { + toggleMenu(); + } + }; + document.addEventListener("click", handleOutsideClick); + + // Cleanup + return () => { + document.removeEventListener("click", handleOutsideClick); + }; +} +``` + + + +```python filename="streamlit_app.py" +import streamlit as st +from radial_menu_component import radial_menu + +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.addEventListener("click", () => { + 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 +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); + // ... + 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, 6)) - 90deg); + transform: rotate(var(--angle)) translate(var(--radius)) + rotate(calc(-1 * var(--angle))); +} +``` + +### Cleanup for document listeners + +Since the component adds a document-level click handler, it must be removed in the cleanup function: + +```javascript +return () => { + document.removeEventListener("click", handleOutsideClick); +}; +``` + +## Related documentation + +- [Component registration](/develop/concepts/custom-components/components-v2/register) +- [Component mounting](/develop/concepts/custom-components/components-v2/mount) +- [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate) +- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) 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..75e4e1a25 --- /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 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..eaaa833ea --- /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 + + +my_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 = my_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..215854145 --- /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}") + + +my_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 = False + +result = my_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 = my_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..6e55c7d02 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/examples/text-input.md @@ -0,0 +1,251 @@ +--- +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 and pasting, expand the complete code below. However, for easier reading, the code is split into multiple files in the next section. + + + +```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); + }; + } +""" + +text_component = st.components.v2.component( + "my_text_input", + html=HTML, + js=JS, +) + + +def text_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 = text_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result + + +if st.button("Hello World"): + st.session_state["my_text_input_instance"]["value"] = "Hello World" +if st.button("Clear text"): + st.session_state["my_text_input_instance"]["value"] = "" +result = text_component_wrapper( + "Enter something", + default="I love Streamlit!", + key="my_text_input_instance", +) + +st.write("Result:", result) +st.write("Session state:", st.session_state) +``` + + + +## File-based version + +```none filename="Directory structure" +my_app/ +├── streamlit_app.py +└── my_component/ + ├── __init__.py + ├── component.html + └── component.js +``` + +```markup filename="my_component/component.html" + + +``` + +```javascript filename="my_component/component.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); + }; +} +``` + +```python filename="my_component/__init__.py" +from pathlib import Path +import streamlit as st + +component_dir = Path(__file__).parent + +@st.cache_data +def load_component_code(): + with open(component_dir / "component.html", "r") as f: + HTML = f.read() + with open(component_dir / "component.js", "r") as f: + JS = f.read() + return HTML, JS + +HTML, JS = load_component_code() + +text_component = st.components.v2.component( + name="my_text_input", + html=HTML, + js=JS, +) + +def text_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 = text_component( + data=data, + default={"value": value}, + key=key, + on_value_change=on_change, + ) + return result +``` + +```python filename="streamlit_app.py" +from my_component import text_component_wrapper + +if st.button("Hello World"): + st.session_state["my_text_input_instance"]["value"] = "Hello World" +if st.button("Clear text"): + st.session_state["my_text_input_instance"]["value"] = "" +result = text_component_wrapper( + "Enter something", + default="I love Streamlit!", + key="my_text_input_instance", +) + +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 my_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 = my_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_text_input_instance"]["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. + +## Related documentation + +- [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate) +- [Component mounting](/develop/concepts/custom-components/components-v2/mount) +- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) 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..0ab7c5e2b --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/mount.md @@ -0,0 +1,274 @@ +--- +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, BidiComponentCallable, 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="my_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 + +my_component = st.components.v2.component( + name="my_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 = my_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 + +simple_component = st.components.v2.component( + name="simple_checkbox", + html="""""", + js=""" + export default function({ parentElement, data, setStateValue, key }) { + const checkbox = parentElement.querySelector("input[type='checkbox']"); + const enabled = data.enabled; + + // Initialize checkbox state + checkbox.checked = enabled; + + // Update state when checkbox is toggled + checkbox.addEventListener("change", () => { + setStateValue("enabled", checkbox.checked); + }); + } + """ +) + +initial_state = True + +result = simple_component( + data={"enabled": initial_state}, + default={"enabled": initial_state}, + on_enabled_change=lambda: None +) +``` + +## Mounting parameters + +All mounting parameters are keyword-only and optional. The available parameters are documented in the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) 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 `ComponentArgs` 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_enabled_change=lambda: None`. Because the callback is defined, even trivially, the result returned by the component will always have an `enabled` attribute. The default value is configured with the `data` parameter and is `True`. + + + +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 `"enabled"` state: + +```python +initial_state = True + +result = simple_component( + data={"enabled": initial_state}, + default={"enabled": initial_state}, + on_enabled_change=lambda: None +) +``` + +In the component's JavaScript function, the initial DOM state is set from the `"enabled"` key in the `data` parameter: + +```javascript +export default function ({ parentElement, data, setStateValue, key }) { + const checkbox = parentElement.querySelector("input[type='checkbox']"); + const enabled = data.enabled; + + // Initialize checkbox state + checkbox.checked = enabled; + + // Update state when checkbox is toggled + checkbox.addEventListener("change", () => { + setStateValue("enabled", checkbox.checked); + }); +} +``` + +In the previous example, if your remove `default={"enabled": initial_state},` from the Python code, then the initial state of the `"enabled"` 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("enabled", enabled);` to the JavaScript code to ensure that the initial state is set correctly. + +```diff +export default function({ parentElement, data, setStateValue, key }) { + const checkbox = parentElement.querySelector("input[type='checkbox']"); + const enabled = data.enabled; + + // Initialize checkbox state + checkbox.checked = enabled; ++ setStateValue("enabled", enabled); + + // Update state when checkbox is toggled + checkbox.addEventListener("change", () => { + setStateValue("enabled", 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` 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.bidicomponentresult) 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..fc774bfaa --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/package-based.md @@ -0,0 +1,744 @@ +--- +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, Webpack, pyproject.toml, npm packages, component distribution +--- + +# 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. + +## When to use package-based components + +Choose package-based development when you need: + +- 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. + +## Project structure + +A typical package-based component follows this structure: + +``` +my-component-package/ +├── pyproject.toml # Top-level package configuration +└── src/ + └── my_component/ + ├── __init__.py # Python package entry point + ├── component.py # Component Python API + ├── pyproject.toml # Component-specific configuration + └── frontend/ + ├── dist/ # Built frontend assets + │ ├── bundle-.js + │ └── styles-.css + ├── src/ # Frontend source code + │ ├── index.ts # Main TypeScript entry + │ └── components/ + ├── package.json # Frontend dependencies + ├── tsconfig.json # TypeScript configuration + └── vite.config.js # Build tool configuration +``` + +## Configuration setup + +### Top-level `pyproject.toml` + +Configure your Python package distribution. This file is located at the root of your project and is used to configure the package distribution. For more information about the `pyproject.toml` for packaging projects, see the [Python Packaging User Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/). + +#### Explicit package configuration (recommended) + +This approach explicitly lists packages and their locations. You need to identify each component module and the necessary assets to serve (frontend components and inner `pyproject.toml` file). + +```toml +[project] +name = "my_streamlit_component_package" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = ["streamlit>=1.51.0"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +# Explicitly list packages and their source directory +[tool.setuptools] +packages = ["my_component"] # List each package by name +package-dir = {"" = "src"} # Look for packages in src/ directory +include-package-data = true # Include non-Python files + +# Specify which files to include in the package +[tool.setuptools.package-data] +my_component = ["frontend/dist/**/*", "pyproject.toml"] +``` + +#### Alternative: Automatic package discovery + +For projects with multiple packages or complex structures, you can use automatic discovery: + +```toml +[project] +name = "my_streamlit_component_package" +version = "0.1.0" +requires-python = ">=3.10" +dependencies = ["streamlit>=1.51.0"] + +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +# Automatically find packages matching a pattern +[tool.setuptools.packages.find] +where = ["src"] # Look in src/ directory +include = ["my_component*"] # Include packages starting with "my_component" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.package-data] +my_component = ["frontend/dist/**/*", "pyproject.toml"] +``` + +### Component-level `pyproject.toml` + +Within your component module, you need to register your component and specify the asset directory (`asset_dir`) in the `[tool.streamlit.component.components]` table. The `asset_dir` path is relative to the component's `pyproject.toml` file. All files and subdirectories within this directory will be served by Streamlit. + +When you start a Streamlit app, Streamlit scans all installed packages for any Streamlit components. For each installed component, Streamlit serves the contents of its asset directory. This makes it possible to refer to images and other assets within your component's HTML and CSS code. `project.name` should match the name of your package when installed. + +```toml +[project] +name = "my_streamlit_component_package" +version = "0.1.0" + +# Register your components and the asset directory. +[[tool.streamlit.component.components]] +name = "my_component" +asset_dir = "frontend/dist" +``` + + + +The `asset_dir` path is relative to the component's `pyproject.toml` file. All files and subdirectories within this 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 development setup + +### `package.json` configuration + +Set up your frontend dependencies and build scripts: + +```json +{ + "name": "my-component-frontend", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "type-check": "tsc --noEmit" + }, + "dependencies": { + "@streamlit/component-v2-lib": "^0.1.0" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "typescript": "^5.0.0", + "vite": "^5.0.0" + } +} +``` + +### TypeScript configuration + +Configure TypeScript for optimal development: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### Vite build configuration + +Configure Vite for optimized builds with hashed filenames: + +```javascript +// vite.config.js +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + outDir: "dist", + lib: { + entry: "src/index.ts", + name: "MyComponent", + fileName: (format) => + `bundle-[hash].${format === "es" ? "js" : "umd.js"}`, + formats: ["es"], + }, + rollupOptions: { + output: { + assetFileNames: (assetInfo) => { + if (assetInfo.name?.endsWith(".css")) { + return "styles-[hash].css"; + } + return "[name]-[hash].[ext]"; + }, + }, + }, + sourcemap: true, + }, +}); +``` + +## TypeScript component development + +### Basic TypeScript component + +Create a type-safe component using the official TypeScript library: + +```typescript +// src/index.ts +import { Component, ComponentState } from "@streamlit/component-v2-lib"; + +/** The state/trigger values this component maintains */ +interface MyComponentState extends ComponentState { + count: number; + lastAction: string; +} + +/** The shape of the data passed from Python */ +interface MyComponentData { + initialCount: number; + label: string; + theme: "light" | "dark"; +} + +const MyComponent: Component = ( + component, +) => { + const { data, setStateValue, setTriggerValue, parentElement } = component; + + let count = data.initialCount || 0; + + // Create UI elements + const container = document.createElement("div"); + container.className = "component-container"; + + const display = document.createElement("div"); + display.className = "count-display"; + display.textContent = `Count: ${count}`; + + const incrementBtn = document.createElement("button"); + incrementBtn.textContent = `${data.label || "Increment"}`; + incrementBtn.className = "increment-btn"; + + const resetBtn = document.createElement("button"); + resetBtn.textContent = "Reset"; + resetBtn.className = "reset-btn"; + + // Assemble UI + container.appendChild(display); + container.appendChild(incrementBtn); + container.appendChild(resetBtn); + parentElement.appendChild(container); + + // Apply theme + container.setAttribute("data-theme", data.theme || "light"); + + // Event handlers with type safety + const handleIncrement = (): void => { + count++; + display.textContent = `Count: ${count}`; + setStateValue("count", count); + setTriggerValue("lastAction", "increment"); + }; + + const handleReset = (): void => { + count = 0; + display.textContent = `Count: ${count}`; + setStateValue("count", count); + setTriggerValue("lastAction", "reset"); + }; + + // Attach event listeners + incrementBtn.addEventListener("click", handleIncrement); + resetBtn.addEventListener("click", handleReset); + + // Initialize state + setStateValue("count", count); + + // Return cleanup function + return () => { + incrementBtn.removeEventListener("click", handleIncrement); + resetBtn.removeEventListener("click", handleReset); + }; +}; + +export default MyComponent; +``` + +### Advanced component with external dependencies + +Here's an example using Chart.js for data visualization: + +```typescript +// src/chart-component.ts +import { Component, ComponentState } from "@streamlit/component-v2-lib"; +import { Chart, ChartConfiguration, registerables } from "chart.js"; + +// Register Chart.js components +Chart.register(...registerables); + +interface ChartComponentState extends ComponentState { + selectedDataPoint: number | null; +} + +interface ChartData { + labels: string[]; + datasets: Array<{ + label: string; + data: number[]; + backgroundColor?: string; + borderColor?: string; + }>; +} + +interface ChartComponentData { + chartData: ChartData; + chartType: "line" | "bar" | "pie"; + title?: string; +} + +const ChartComponent: Component = ( + component, +) => { + const { data, setStateValue, setTriggerValue, parentElement } = component; + + // Create canvas element + const canvas = document.createElement("canvas"); + canvas.width = 400; + canvas.height = 300; + parentElement.appendChild(canvas); + + // Chart configuration + const config: ChartConfiguration = { + type: data.chartType || "line", + data: data.chartData, + options: { + responsive: true, + plugins: { + title: { + display: !!data.title, + text: data.title, + }, + legend: { + position: "top", + }, + }, + onClick: (event, elements) => { + if (elements.length > 0) { + const dataIndex = elements[0].index; + setStateValue("selectedDataPoint", dataIndex); + setTriggerValue("dataPointClicked", { + index: dataIndex, + label: data.chartData.labels[dataIndex], + value: data.chartData.datasets[0].data[dataIndex], + }); + } + }, + }, + }; + + // Create chart instance + const chart = new Chart(canvas, config); + + // Cleanup function + return () => { + chart.destroy(); + }; +}; + +export default ChartComponent; +``` + +## Python component API + +### Component definition + +Create a clean Python API for your component: + +```python +# src/my_component/component.py +import streamlit as st +from typing import Dict, Any, Optional, Callable, Union, List + +def advanced_counter( + initial_value: int = 0, + label: str = "Increment", + theme: str = "light", + key: Optional[str] = None, + on_count_change: Optional[Callable] = None, + on_lastAction_change: Optional[Callable] = None +): + """ + Create an advanced counter component with TypeScript frontend. + + Parameters + ---------- + initial_value : int + The starting count value (default: 0) + label : str + The text to display on the increment button (default: "Increment") + theme : str + The component theme, either "light" or "dark" (default: "light") + key : str, optional + A unique key for the component instance + on_count_change : callable, optional + Callback function called when count changes + on_lastAction_change : callable, optional + Callback function called when an action is triggered + + Returns + ------- + ComponentResult + Object with count and lastAction properties + """ + + # Create the component using glob pattern for hashed builds + component = st.components.v2.component( + name="advanced_counter", + js="bundle-*.js", # Glob pattern matches hashed filename + css="styles-*.css", # Glob pattern matches hashed CSS + data={ + "initialCount": initial_value, + "label": label, + "theme": theme + } + ) + + # Mount the component + result = component( + key=key, + default={"count": initial_value, "lastAction": None}, + on_count_change=on_count_change, + on_lastAction_change=on_lastAction_change + ) + + return result + +def chart_component( + chart_data: Dict[str, Any], + chart_type: str = "line", + title: Optional[str] = None, + key: Optional[str] = None, + on_selectedDataPoint_change: Optional[Callable] = None, + on_dataPointClicked_change: Optional[Callable] = None +): + """ + Create an interactive chart component using Chart.js. + + Parameters + ---------- + chart_data : dict + Chart data in Chart.js format with labels and datasets + chart_type : str + Type of chart: "line", "bar", or "pie" (default: "line") + title : str, optional + Chart title to display + key : str, optional + A unique key for the component instance + on_selectedDataPoint_change : callable, optional + Callback when a data point is selected + on_dataPointClicked_change : callable, optional + Callback when a data point is clicked + + Returns + ------- + ComponentResult + Object with selectedDataPoint and dataPointClicked properties + """ + + component = st.components.v2.component( + name="chart_component", + js="chart-bundle-*.js", + css="chart-styles-*.css", + data={ + "chartData": chart_data, + "chartType": chart_type, + "title": title + } + ) + + result = component( + key=key, + default={"selectedDataPoint": None}, + on_selectedDataPoint_change=on_selectedDataPoint_change, + on_dataPointClicked_change=on_dataPointClicked_change + ) + + return result +``` + +### Package entry point + +Create a clean package interface: + +```python +# src/my_component/__init__.py +""" +My Streamlit Component Package + +A collection of advanced custom components built with TypeScript and modern tooling. +""" + +from .component import advanced_counter, chart_component + +__version__ = "0.1.0" +__all__ = ["advanced_counter", "chart_component"] +``` + +## Glob pattern support + +Package-based components support glob patterns for referencing build outputs with hashed filenames: + +### Why use glob patterns? + +Modern build tools like Vite and Webpack generate hashed filenames for cache busting: + +``` +frontend/dist/ +├── bundle-a1b2c3d4.js # Hashed JavaScript bundle +├── styles-e5f6g7h8.css # Hashed CSS file +└── assets/ + └── logo-i9j0k1l2.png # Hashed assets +``` + +### Glob resolution rules + +1. Pattern matching: `bundle-*.js` matches `bundle-a1b2c3d4.js` +2. Single file requirement: 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 +# These glob patterns work with hashed build outputs +component = st.components.v2.component( + name="my_component", + js="bundle-*.js", # Matches bundle-.js + css="styles-*.css", # Matches styles-.css + data={"message": "Hello"} +) +``` + + + +**Error Handling**: If a glob pattern matches zero files or multiple files, Streamlit will raise a clear error message to help you debug the issue. + + + +## Development workflow + +### Development mode + +During development, use Vite's dev server for hot reloading: + +```bash +# Terminal 1: Start frontend dev server +cd src/my_component/frontend +npm run dev + +# Terminal 2: Run Streamlit app +streamlit run app.py +``` + +For development, temporarily use the dev server URL: + +```python +# Development mode (temporary) +component = st.components.v2.component( + name="my_component", + js="http://localhost:5173/src/index.ts", # Dev server URL + data={"message": "Hello"} +) +``` + +### Build for production + +Build optimized assets for production: + +```bash +cd src/my_component/frontend +npm run build +``` + +This generates hashed files in the `dist/` directory that your glob patterns will match. + +### Testing the package + +Test your component locally before publishing: + +```python +# app.py - Test your component +import streamlit as st +from my_component import advanced_counter, chart_component + +st.title("Component Testing") + +# Test the counter +counter_result = advanced_counter( + initial_value=5, + label="Click me!", + theme="dark", + key="test_counter" +) + +st.write(f"Count: {counter_result.count}") +if counter_result.lastAction: + st.write(f"Last action: {counter_result.lastAction}") + +# Test the chart +chart_data = { + "labels": ["Jan", "Feb", "Mar", "Apr", "May"], + "datasets": [{ + "label": "Sales", + "data": [12, 19, 3, 5, 2], + "backgroundColor": "rgba(54, 162, 235, 0.2)", + "borderColor": "rgba(54, 162, 235, 1)" + }] +} + +chart_result = chart_component( + chart_data=chart_data, + chart_type="bar", + title="Monthly Sales", + key="test_chart" +) + +if chart_result.selectedDataPoint is not None: + st.write(f"Selected data point: {chart_result.selectedDataPoint}") +``` + +## Publishing your package + +### Build the distribution + +```bash +# Install build tools +pip install build twine + +# Build the package +python -m build +``` + +### Upload to PyPI + +```bash +# Upload to Test PyPI first +python -m twine upload --repository testpypi dist/* + +# After testing, upload to PyPI +python -m twine upload dist/* +``` + +### Installation and usage + +Users can then install and use your component: + +```bash +pip install my-streamlit-component-package +``` + +```python +import streamlit as st +from my_streamlit_component_package import advanced_counter + +result = advanced_counter( + initial_value=10, + label="Increment Counter", + theme="dark" +) + +st.write(f"Current count: {result.count}") +``` + +## Best practices + +### Type safety + +Always use TypeScript interfaces for better development experience: + +```typescript +interface ComponentProps { + data: MyComponentData; + setStateValue: (key: string, value: any) => void; + setTriggerValue: (key: string, value: any) => void; + parentElement: HTMLElement; +} +``` + +### Error handling + +Implement robust error handling in both TypeScript and Python: + +```typescript +// TypeScript error handling +export default function (component) { + try { + // Component logic here + return () => { + // Cleanup logic + }; + } catch (error) { + console.error("Component error:", error); + component.parentElement.innerHTML = `
Component failed to load
`; + } +} +``` + +### Performance optimization + +- Use code splitting for large dependencies +- Implement lazy loading for heavy components +- Optimize bundle sizes with tree shaking + +### Documentation + +Provide comprehensive documentation: + +- TypeScript interfaces for all data shapes +- Python docstrings with parameter descriptions +- Usage examples and tutorials +- Migration guides for updates + +## What's next? + +Now that you understand package-based components: + +- 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..105eeabb0 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/register.md @@ -0,0 +1,172 @@ +--- +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, ComponentArgs +--- + +# 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 [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) type. +- In your component's CSS, to make your component theme-aware, use [CSS custom properties](/develop/concepts/custom-components/components-v2/theming#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 +my_component = st.components.v2.component( + name="my_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 +my_component = st.components.v2.component( + name="my_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("clicked", true); + }; + } + """ +) +``` + +## 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 [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) 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): 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. + +```none filename="Directory structure" +my_app/ +├── streamlit_app.py +└── my_component/ + ├── component.css + ├── component.html + └── component.js +``` + +```python +# Load HTML, CSS, and JS from external files +@st.cache_data +def load_component_code(): + with open("my_component/my_css.css", "r") as f: + CSS = f.read() + with open("my_component/my_html.html", "r") as f: + HTML = f.read() + with open("my_component/my_js.js", "r") as f: + JS = f.read() + return HTML, CSS, JS + +HTML, CSS, JS = load_component_code() + +file_component = st.components.v2.component( + name="file_based", + html=HTML, + css=CSS, + js=JS, +) +``` + + + +Using `@st.cache_data` is a good practice to avoid reloading the component code on every rerun, but you might want to temporarily remove caching during development. Streamlit will automatically invalidate the cache if you make code changes within a cache-decorated function, but Streamlit can't infer the changes from files that are read. In this case, you must manually clear the cache when you make changes to files you've read within the cached function. For more information, see [Caching](/develop/api-reference/caching). + + + +## 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..ebd32b418 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -0,0 +1,1547 @@ +--- +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. Here's a practical example that demonstrates using a state value to track a selection. The following code creates a radial menu component that allows the user to select a food item from a list of options. When the user selects an item, the component updates the state value with `setStateValue("selection", currentSelection)`. You can expand or collapse the each code block as needed. For emphasis, the JavaScript and example app code are expanded by default. + +For simplicity, this component assumes it will always have six options in its menu, but with a little more code, you can generalize it accept an arbitrary number of items. The complete code provided at the end of this section demonstrates a generalized version that accepts an arbitrary number of items. + +For this example, the component is registered in an imported module. + +``` +project_directory/ +├── radial_menu_component/ +│ ├── __init__.py +│ ├── menu.css +│ ├── menu.html +│ └── menu.js +└── streamlit_app.py +``` + +### Radial menu component registration + + + +```python +from pathlib import Path +import streamlit as st + +component_dir = Path(__file__).parent + + +@st.cache_data +def load_component_code(): + with open(component_dir / "menu.css", "r") as f: + CSS = f.read() + with open(component_dir / "menu.html", "r") as f: + HTML = f.read() + with open(component_dir / "menu.js", "r") as f: + JS = f.read() + return HTML, CSS, JS + + +HTML, CSS, JS = load_component_code() + +radial_menu = st.components.v2.component( + name="radial_menu", + html=HTML, + css=CSS, + js=JS, +) +``` + + + +### Radial menu HTML code + + + +```markup +
+ + + +
+``` + +
+ +### Radial menu CSS code + + + +```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 (6 items at 60 degree intervals)*/ +.menu-item { + --angle: calc(var(--i) * 60deg - 90deg); + + background: var(--st-background-color); + position: absolute; + top: 50%; + left: 50%; + margin: -1.6125rem; + transform: rotate(var(--angle)) translateX(4rem) + rotate(calc(-1 * var(--angle))); +} + +.menu-item:hover { + transform: rotate(var(--angle)) translateX(4rem) + 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 */ +.menu-overlay::before { + content: ""; + position: fixed; + inset: -100vh -100vw; + background: var(--st-background-color); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: -1; +} + +.menu-overlay.open::before { + opacity: 0.7; + pointer-events: auto; +} + +/* 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; +} +``` + + + +### Radial menu JavaScript code + + + +```javascript +export default function ({ parentElement, data, setStateValue }) { + const selector = parentElement.querySelector("#selector"); + const selectorIcon = parentElement.querySelector("#selector-icon"); + const overlay = parentElement.querySelector("#overlay"); + const ring = parentElement.querySelector("#ring"); + + let isOpen = false; + const options = data?.options || {}; + let currentSelection = data?.selection || Object.keys(options)[0]; + + // Create the 6 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.addEventListener("click", () => { + 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, + ); + }); + } + + // Toggle menu open/closed + function toggleMenu() { + isOpen = !isOpen; + overlay.classList.toggle("open", isOpen); + ring.classList.toggle("open", isOpen); + } + + // Initialize display + updateDisplay(); + + // Selector click toggles menu + selector.addEventListener("click", toggleMenu); + + // Click outside closes menu + overlay.addEventListener("click", (e) => { + if (e.target === overlay) toggleMenu(); + }); +} +``` + + + +### Radial menu example app + + + +```python +import streamlit as st +from radial_menu_component import radial_menu + +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()}**") +``` + + + + + +```python +import streamlit as st +from my_component import radial_menu + +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 */ + .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 { + position: relative; + z-index: 10; + } + + .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; + } + + /* Individual menu items - dynamic angle based on --i and --total */ + .menu-item { + --angle: calc(var(--i) * (360deg / var(--total, 6)) - 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 */ + .menu-overlay::before { + content: ''; + position: fixed; + inset: -100vh -100vw; + background: var(--st-background-color); + opacity: 0; + transition: opacity 0.3s ease; + pointer-events: none; + z-index: -1; + } + + .menu-overlay.open::before { + opacity: 0.7; + pointer-events: auto; + } + + /* 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 ring = parentElement.querySelector("#ring"); + + let isOpen = false; + + // Get options and selection from data + const options = data?.options || {}; + let currentSelection = data?.selection || Object.keys(options)[0] || ""; + + // Calculate angle step based on number of options + const optionEntries = Object.entries(options); + const angleStep = 360 / optionEntries.length; + + // Generate menu items dynamically + function createMenuItems() { + ring.innerHTML = ""; + + optionEntries.forEach(([value, icon], index) => { + const button = document.createElement("button"); + button.className = "menu-item"; + button.dataset.value = value; + button.dataset.icon = icon; + button.style.setProperty("--i", index); + button.style.setProperty("--total", optionEntries.length); + button.innerHTML = `${icon}`; + + button.addEventListener("click", (e) => { + e.stopPropagation(); + currentSelection = value; + updateDisplay(); + closeMenu(); + setStateValue("selection", currentSelection); + }); + + ring.appendChild(button); + }); + } + + // Update display based on current selection + function updateDisplay() { + const icon = options[currentSelection] || "?"; + selectorIcon.textContent = icon; + + ring.querySelectorAll(".menu-item").forEach((item) => { + item.classList.remove("selected"); + if (item.dataset.value === currentSelection) { + item.classList.add("selected"); + } + }); + } + + // Calculate position to keep menu in viewport + function calculatePosition() { + const selectorRect = selector.getBoundingClientRect(); + const menuRadius = 125; + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Extra padding at top for Streamlit header + const topPadding = 70; + const edgePadding = 10; + + const centerX = selectorRect.left + selectorRect.width / 2; + const centerY = selectorRect.top + selectorRect.height / 2; + + let offsetX = 0; + let offsetY = 0; + + if (centerX - menuRadius < edgePadding) { + offsetX = menuRadius - centerX + edgePadding; + } else if (centerX + menuRadius > viewportWidth - edgePadding) { + offsetX = viewportWidth - edgePadding - menuRadius - centerX; + } + + if (centerY - menuRadius < topPadding) { + offsetY = menuRadius - centerY + topPadding; + } else if (centerY + menuRadius > viewportHeight - edgePadding) { + offsetY = viewportHeight - edgePadding - menuRadius - centerY; + } + + return { offsetX, offsetY }; + } + + // Open menu with spring animation + function openMenu() { + isOpen = true; + const { offsetX, offsetY } = calculatePosition(); + overlay.style.transform = `translate(calc(-50% + ${offsetX}px), calc(-50% + ${offsetY}px))`; + overlay.classList.add("open"); + ring.classList.remove("closing"); + ring.classList.add("open"); + } + + // Close menu with reverse animation + function closeMenu() { + isOpen = false; + ring.classList.remove("open"); + ring.classList.add("closing"); + overlay.classList.remove("open"); + + setTimeout(() => { + ring.classList.remove("closing"); + overlay.style.transform = "translate(-50%, -50%)"; + }, 300); + } + + // Toggle menu + function toggleMenu() { + if (isOpen) { + closeMenu(); + } else { + openMenu(); + } + } + + // Initialize + createMenuItems(); + updateDisplay(); + + // Selector click + selector.addEventListener("click", (e) => { + e.stopPropagation(); + toggleMenu(); + }); + + // Click on ring background to close + overlay.addEventListener("click", (e) => { + if (e.target === overlay || e.target === ring) { + closeMenu(); + } + }); + + // Click outside to close + const handleOutsideClick = (e) => { + if (isOpen && !parentElement.contains(e.target)) { + closeMenu(); + } + }; + document.addEventListener("click", handleOutsideClick); + + // Cleanup + return () => { + document.removeEventListener("click", handleOutsideClick); + }; + } + """, +) + +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()}**") +``` + +
+ +## Trigger values in practice + +Trigger values are ideal for handling discrete user actions. The following example creates a button that requires the user to hold for two seconds to confirm the action. Only when the user continuously holds the button for two seconds will the component update the trigger value with `setTriggerValue("deleted", true)`. The component also displays a progress ring to indicate the user's progress. + +```python +import streamlit as st + +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 = [] + +danger_button = st.components.v2.component( + name="hold_to_confirm", + html=""" +
+
+ ⚠️ + Danger Zone +
+ + + +

Press and hold for 2 seconds to confirm

+
+ """, + css=""" + .danger-zone { + font-family: var(--st-font); + padding: 2rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; + } + + .warning-banner { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: var(--st-red-background-color); + border: 1px solid var(--st-red-color); + border-radius: var(--st-base-radius); + } + + .warning-icon { + font-size: 1rem; + } + + .warning-text { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--st-red-color); + } + + .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 6px var(--st-red-color)); + } + + .button-content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + } + + .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; + } + + .hint { + font-size: 0.7rem; + color: var(--st-text-color); + opacity: 0.5; + margin: 0; + } + """, + js=""" + export default function({ parentElement, setTriggerValue }) { + const button = parentElement.querySelector("#danger-btn"); + const progress = parentElement.querySelector("#ring-progress"); + const icon = parentElement.querySelector("#icon"); + const label = parentElement.querySelector("#label"); + + const HOLD_DURATION = 2000; // 2 seconds + const COOLDOWN_DURATION = 1500; // cooldown after trigger + const CIRCUMFERENCE = 2 * Math.PI * 45; // circle circumference + + 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 = "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } + + function cancelHold() { + if (isDisabled) return; // Ignore if in cooldown + + startTime = null; + button.classList.remove("holding"); + label.textContent = "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 = "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 = "🗑️"; + label.textContent = "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; + }, COOLDOWN_DURATION); + } + + // Mouse events + button.addEventListener("mousedown", startHold); + button.addEventListener("mouseup", cancelHold); + button.addEventListener("mouseleave", cancelHold); + + // Touch events for mobile + button.addEventListener("touchstart", (e) => { + e.preventDefault(); + startHold(); + }); + button.addEventListener("touchend", cancelHold); + button.addEventListener("touchcancel", cancelHold); + + return () => { + if (animationFrame) cancelAnimationFrame(animationFrame); + }; + } + """, +) + + +# 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 +result = danger_button(key="danger_btn", on_confirmed_change=on_delete_confirmed) + +# 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}") +``` + +## 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..d1217a0e5 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -0,0 +1,546 @@ +--- +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. + +## 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 you component in 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` | + +### 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` | + +## 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`, which wraps your component in a Shadow DOM: + +```python +# Styles are isolated (default behavior) +isolated_component = st.components.v2.component( + name="isolated", + html="
Isolated content
", + css=".my-style { color: red; }", # Won't affect other elements + isolate_styles=True # Default +) +``` + +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`. This is uncommon. + +```python +# Styles can affect the page +non_isolated_component = st.components.v2.component( + name="non_isolated", + html="
Content with inheritance
", + css=".inherits-styles { font-family: inherit; }", # Inherits page fonts + 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 0569cc83e..94072e85e 100644 --- a/content/menu.md +++ b/content/menu.md @@ -98,14 +98,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 f1141914c..6c7582001 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 @@ -1154,6 +1154,10 @@ /develop/tutorials/llms/build-conversational-apps /develop/tutorials/chat-and-llm-apps/build-conversational-apps /develop/tutorials/llms/llm-quickstart /develop/tutorials/chat-and-llm-apps/llm-quickstart /knowledge-base/deploy/authentication-without-sso /develop/concepts/connections/authentication +/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