From 48696ee6ab531d3bd5888f5e435c9e24a2bbb860 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Thu, 4 Dec 2025 15:34:45 -0800 Subject: [PATCH 01/26] First draft of ccv2 concepts --- .../concepts/custom-components/_index.md | 66 +- .../custom-components/components-v1/_index.md | 62 ++ .../v1-component-api.md} | 2 +- .../v1-component-create.md} | 2 +- .../v1-component-limitations.md} | 2 +- .../custom-components/components-v2/_index.md | 67 ++ .../custom-components/components-v2/create.md | 393 +++++++++ .../components-v2/package-based.md | 744 ++++++++++++++++++ .../components-v2/quickstart.md | 407 ++++++++++ .../components-v2/state-and-triggers.md | 548 +++++++++++++ .../components-v2/theming.md | 554 +++++++++++++ .../concepts/custom-components/overview.md | 53 ++ .../{publish-component.md => publish.md} | 0 content/menu.md | 30 +- public/_redirects | 130 +-- 15 files changed, 2949 insertions(+), 111 deletions(-) create mode 100644 content/develop/concepts/custom-components/components-v1/_index.md rename content/develop/concepts/custom-components/{components-api.md => components-v1/v1-component-api.md} (99%) rename content/develop/concepts/custom-components/{create-component.md => components-v1/v1-component-create.md} (96%) rename content/develop/concepts/custom-components/{limitations.md => components-v1/v1-component-limitations.md} (96%) create mode 100644 content/develop/concepts/custom-components/components-v2/_index.md create mode 100644 content/develop/concepts/custom-components/components-v2/create.md create mode 100644 content/develop/concepts/custom-components/components-v2/package-based.md create mode 100644 content/develop/concepts/custom-components/components-v2/quickstart.md create mode 100644 content/develop/concepts/custom-components/components-v2/state-and-triggers.md create mode 100644 content/develop/concepts/custom-components/components-v2/theming.md create mode 100644 content/develop/concepts/custom-components/overview.md rename content/develop/concepts/custom-components/{publish-component.md => publish.md} (100%) diff --git a/content/develop/concepts/custom-components/_index.md b/content/develop/concepts/custom-components/_index.md index fd0c24a3c..770b95ad9 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 v1

-4. ...now you're ready to use it! For AgGrid, that's: +The original custom components framework. Learn how to use and build v1 components. - ```python - AgGrid(my_dataframe) - ``` +
-## Making your own Component + -If you're interested in making your own component, check out the following resources: +

Components v2

-- [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 next generation of custom components with enhanced capabilities, bidirectional communication, and simplified development. -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..83f5623d9 --- /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/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/) + +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..3da2a2c71 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/_index.md @@ -0,0 +1,67 @@ +--- +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. For 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. + +
+ + + +

Create components

+ +Learn the basics of registering and mounting v2 components with inline development. + +
+ + + +

State vs triggers

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

Theming & 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/quickstart) to see practical implementations, then dive into the [Create components](/develop/concepts/custom-components/components-v2/create) guide to learn the fundamentals. diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md new file mode 100644 index 000000000..cd05c44e1 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -0,0 +1,393 @@ +--- +title: Create custom v2 components +slug: /develop/concepts/custom-components/components-v2/create +description: Learn how to create Streamlit custom components v2 with inline development, from basic registration to mounting components with rich interactions. +keywords: custom components v2, create components, inline components, component registration, component mounting, bidirectional communication +--- + +# Create custom v2 component + +Components v2 provides a modern, flexible approach to extending Streamlit with custom functionality. This guide will walk you through creating your first component using the inline development approach. For package-based components, see the [Package-based Components](/develop/concepts/custom-components/components-v2/package-based) guide. + +## Two-step component process + +Creating and using a custom component involves two distinct steps: + +1. **Registration**: Define your component's structure (HTML, CSS, JavaScript). +2. **Mounting**: Create and display an instance of your component in your app with specific data. + +## Step 1: Component registration + +Registration is where you define what your component looks like and how it behaves. Use [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component) to register a component: + +```python +import streamlit as st + +# Register a component +my_component = st.components.v2.component( + name="my_button", + html="", + css="button { padding: 10px; background: blue; color: white; }", + js=""" + export default function(component) { + const { parentElement, setTriggerValue } = component; + + parentElement.querySelector('button').onclick = () => { + setTriggerValue('clicked', true); + }; + } + """ +) +``` + +### Registration parameters + +- The `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. +- The `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. It can be inline HTML code or a path to an `.html` file. +- The `css` (optional) is the CSS styling for your component. It can be inline CSS code or a path to a `.css` file. +- The `js` (optional) is the JavaScript logic for your component. It can be inline JavaScript code or a path to a `.js` file. + + + +A component must have either `html`, `js`, or both defined! You cannot 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 + + // Optional: return cleanup function + return () => { + // Cleanup logic (remove event listeners, clear timers, etc.) + }; +} +``` + +The `component` parameter provides these essential properties as documented 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 this component instance. Use this to assist with tracking unique instances of your component in the DOM. +- `data` (any): All data passed from Python via the `data` parameter. Use this to customize a component instance. +- `parentElement` (HTMLElement): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements. +- `setStateValue` (function): JS function to communicate stateful values to your Python backend. +- `setTriggerValue` (function): JS function to communicate event-based trigger values to your Python backend. + + + +Don't directly modify the inner HTML of the `parentElement`. This will delete the HTML, CSS, and JavaScript code that was registered with the component. If you need to inject content from `data`, create a placeholder element within `html` and update its content with JavaScript. + + + +### Basic registration examples + +#### Simple HTML component + +In the following examples, we'll register a simple component that displays "Hello, World!" in a heading. We use the primary color from the Streamlit theme for the heading color. For more information about making your components theme-aware, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. + +```python +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }" +) +``` + +#### File-based component + +For larger components, you can organize your code into separate files: + +``` +my_app/ +├── streamlit_app.py # Entrypoint file +├── styles/ +│ └── component.css # Component styles +└── scripts/ + └── component.js # Component JavaScript +``` + +```python +# Load CSS and JS from external files +file_component = st.components.v2.component( + name="file_based", + html="
Loading...
", + css="./styles/component.css", # Path to CSS file + js="./scripts/component.js" # Path to JS file +) +``` + +#### Interactive component + +In the following example, we'll register a component that displays a counter and a button to increment the counter. The counter value is stored in the component's state and is updated when the button is clicked. The component also triggers an event when the button is clicked. The component properties are destructured within the function signature directly. + +```python +import streamlit as st + +counter_component = st.components.v2.component( + name="counter", + html=""" +
+ 0 + +
+ """, + js=""" + export default function({ parentElement, setStateValue }) { + let count = 0; + const display = parentElement.querySelector('#count'); + const button = parentElement.querySelector('#increment'); + + button.onclick = () => { + count++; + display.textContent = count; + setStateValue('count', count); + }; + } + """ +) +``` + +## Step 2: Component mounting + +After registration, you mount your component in your Streamlit app. This creates a specific instance of the component and is equivalent to calling native Streamlit commands like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output: + +```python +# Mount the component +result = my_component( + key="unique_instance", + data={"initial_value": 42}, + on_clicked_change=lambda: st.write("Button was clicked!") +) +``` + +### 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 iterate on the existing component instance when other parameters change. +- If you don't pass a key when you mount your component, Streamlit will create a new component instance when other parameters change. This will reset stateful values. + +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") +``` + + + +The `key` property available 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. + + + +#### Customizing and updating an instance (`data` and `default`) + +A component instance can be customized and updated through two parameters in its mounting command that pass data between Python and JavaScript. + +The `data` parameter passes information from Python to your component's frontend. It suppores JSON-serizable, Arrow-seriable, and raw bytes data. Commonly this is a single value or a dictionary of values that you retrieve in your JavaScript function. + +The `default` parameter sets initial values for component state. This is a dictionary where each key is a state attribute with an accompanying callback function passed as a keyword argument named `on__change`. + +```python +import pandas as pd + +# Create sample data +df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) + +result = my_component( + data={ + "user_name": "Alice", + "settings": {"theme": "dark", "notifications": True}, + "dataframe": df, # Auto-converted to Arrow format + "raw_bytes": b"binary data", + "nested_data": { + "level1": { + "level2": ["item1", "item2", "item3"] + } + }, + "number_list": [1, 2, 3, 4, 5] + }, + default={"count": 0, "selected_item": None}, + on_count_change=handle_count_change, + on_selected_item_change=handle_selection +) +``` + + + +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. + + + +#### 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 wrap your component in a `
` element that behaves like other Streamlit elements, but you are responsible for ensuring that the content within your component is responsive to the surrounding `
`. + +```python +result = my_component( + width="stretch", # Full width + height=400 # Fixed height +) +``` + +#### 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. + +```python +result = my_component( + isolate_styles=True # Default behavior uses a shadow root +) +``` + +For more information about theming and styling, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. + +#### Event callbacks (`on__change` or `on__change`) + +For each state and trigger value for your component, you must pass a callback function. This callback function ensures that all state and trigger values are consistently available in the component's result object. Use the name of the state or trigger value in a keyword argument named `on__change`. These callback function can be empty (`lambda: None`) or contain your own response logic. Whenever your JavaScript code calls `setStateValue()` or `setTriggerValue()`, your app will immediately rerun, executing the associated callback as a prefix. Therefore, you can't call both `setStateValue()` and `setTriggerValue()` in response to the same event. + +Continuing the [Interactive component](#interactive-component) example from the previous section, we add a callback function for the `count` state value. + +```python +# Define callback function for the count state value +def handle_count_change(): + # Called when the component calls setStateValue('count', value) + st.toast("Count was updated!") + +# Mount the counter component with callback +result = counter_component( + on_count_change=handle_count_change, + key="counter_1" +) +``` + +## 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 + +Components return a [`BidiComponentResult`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentresult) object that provides access to component state and trigger values. From the previous example, you can access the `count` state value as `result.count`. + +```python +# Access the current count value +st.write(f"Current count: {result.count}") +``` + +### Component values in Session State + +If you mounted your component with a key, you can access the component values through Session State. In the previous example, you can equivalently access the `count` state value as `st.session_state.counter_1.count`. + +```python +# Access the current count value +st.write(f"Current count: {st.session_state.counter_1.count}") +``` + +### 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 Triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) guide. + +## Interactive counter complete example + +Here's the complete example from the previous sections that demonstrates both registration and mounting. We've added some minimal CSS to make the component look more Streamlit-like and theme-compatible. For more information about theming and styling, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. + +```python +import streamlit as st + +counter_component = st.components.v2.component( + name="counter", + html=""" +
+ 0 + +
+ """, + css=""" + .counter { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + } + #count { + font-size: var(--st-base-font-size); + color: var(--st-text-color); + } + #increment { + background: var(--st-primary-color); + color: white; + border: none; + border-radius: var(--st-button-radius); + padding: 4px 8px; + cursor: pointer; + font-family: var(--st-font); + font-size: var(--st-base-font-size); + } + #increment:hover { + opacity: 0.8; + } + """, + js=""" + export default function({ parentElement, setStateValue }) { + let count = 0; + const display = parentElement.querySelector('#count'); + const button = parentElement.querySelector('#increment'); + + button.onclick = () => { + count++; + display.textContent = count; + setStateValue('count', count); + }; + } + """ +) + +# Define callback function for the count state value +def handle_count_change(): + # Called when the component calls setStateValue('count', value) + st.toast("Count was updated!") + +# Mount the counter component with callback +result = counter_component( + on_count_change=handle_count_change, + key="counter_1" +) + +# Access the current count value +st.write(f"Current count: {result.count}") + +# Access the current count value in Session State +st.write(f"Current count: {st.session_state.counter_1.count}") +``` + +## What's next? + +Now that you understand the basics of component registration and mounting: + +- Learn about [State vs triggers](/develop/concepts/custom-components/v2/state-and-triggers) for advanced component communication. +- Explore [Theming and styling](/develop/concepts/custom-components/v2/theming) to make your components look great. +- Discover [Package-based components](/develop/concepts/custom-components/v2/package-based) for complex projects. +- Check out the [JavaScript API reference](/develop/api-reference/custom-components/) for complete frontend documentation. 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..24d713e10 --- /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/v2/state-and-triggers) for interactive functionality. +- Explore [Theming and styling](/develop/concepts/custom-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/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md new file mode 100644 index 000000000..a22463196 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -0,0 +1,407 @@ +--- +title: Quickstart examples +slug: /develop/concepts/custom-components/components-v2/quickstart +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 that demonstrate the key features and capabilities. + +## Two-step component process + +Creating and using a custom component involves two distinct steps: + +1. Register your component to define its structure (HTML, CSS, JavaScript). + - Register a component with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). + - Within your component's JavaScript function, communicate with Python by destructuring a [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) object. + +2. Mount your component to create a specific instance in your app. + - Use your component command that implements the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) type. + +For more information, see [Create components](/develop/concepts/custom-components/components-v2/create). + +## Simple interactive button + +This example shows the basics of creating an interactive component with bidirectional communication: + +```python +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( + "interactive_button", + html="", + 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}") +``` + +**Key concepts demonstrated:** + +- Component registration with HTML and JavaScript +- Trigger values using `setTriggerValue()` +- Callback functions with `on_{event}_change` pattern +- Session state integration + +## Rich data exchange + +This example shows how to pass different data types to your component: + +```python +import pandas as pd +import streamlit as st + +# Create sample data +df = pd.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "age": [25, 30, 35], + "city": ["New York", "London", "Tokyo"] +}) + +# Pass multiple DataFrames, JSON, or raw bytes - automatically handled +chart_component = st.components.v2.component( + "data_display", + html="
Loading data...
", + js=""" + export default function({ data, parentElement }) { + const container = parentElement.querySelector('#data-container'); + + // Access different data types + const userInfo = data.user_info; + const dataframe = data.df; // Automatically converted from pandas + const settings = data.settings; + + container.innerHTML = ` +

User: ${userInfo.name}

+

DataFrame rows: ${dataframe.length}

+

Theme: ${settings.theme}

+ `; + } + """ +) + +result = chart_component( + data={ + "df": df, # Converted to Arrow format + "user_info": {"name": "Alice"}, # Passed as JSON + "settings": {"theme": "dark"}, # Passed as JSON + "binary_data": b"raw bytes" # Binary data support + } +) +``` + +**Key concepts demonstrated:** + +- Automatic dataframe conversion to Arrow format +- JSON data passing +- Binary data support +- Accessing data in JavaScript via `component.data` + +## Complete interactive counter + +This comprehensive example demonstrates both state and trigger values: + +```python +import streamlit as st + +# Interactive counter with both state and triggers +counter = st.components.v2.component( + "interactive_counter", + html=""" +
+

Count: 0

+
+ + + +
+
+ """, + css=""" + .counter { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + text-align: center; + } + .buttons { + margin-top: 15px; + } + button { + margin: 0 5px; + padding: 8px 16px; + 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(); + + // Cleanup function + return () => { + incrementBtn.removeEventListener('click', incrementBtn.onclick); + decrementBtn.removeEventListener('click', decrementBtn.onclick); + resetBtn.removeEventListener('click', resetBtn.onclick); + }; + } + """ +) + +# Define callbacks +def handle_reset(): + st.balloons() + st.success("Counter reset!") + +# Use with callbacks +result = counter( + data={"initialCount": 0}, + on_count_change=lambda: None, # Track count state + on_reset_change=handle_reset # 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.info("Reset button was clicked!") +``` + +**Key concepts demonstrated:** + +- Both state values (`setStateValue`) and trigger values (`setTriggerValue`) +- Theme integration with CSS custom properties +- Data passing from Python to JavaScript +- Multiple event handlers and callbacks +- Cleanup functions for proper resource management +- Difference between persistent state and transient triggers + +## Form with validation + +This example shows a more complex component with form validation: + +```python +import streamlit as st + +form_component = st.components.v2.component( + "contact_form", + html=""" +
+

Contact Form

+
+ + + +
+ + +
+
+
+
+ """, + css=""" + .form-container { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + max-width: 500px; + } + input, textarea { + width: 100%; + padding: 10px; + margin: 10px 0; + border: 1px solid var(--st-border-color); + border-radius: 4px; + font-family: var(--st-font); + box-sizing: border-box; + } + textarea { + height: 100px; + resize: vertical; + } + .form-actions { + display: flex; + gap: 10px; + margin-top: 15px; + } + button { + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-family: var(--st-font); + } + button[type="submit"] { + background: var(--st-primary-color); + color: white; + } + button[type="button"] { + background: var(--st-secondary-background-color); + color: var(--st-text-color); + border: 1px solid var(--st-border-color); + } + #status { + margin-top: 10px; + font-size: 14px; + } + """, + js=""" + export default function({ parentElement, setStateValue, setTriggerValue, data }) { + const form = parentElement.querySelector('#contact-form'); + const nameInput = parentElement.querySelector('#name'); + const emailInput = parentElement.querySelector('#email'); + const messageInput = parentElement.querySelector('#message'); + const saveDraftBtn = parentElement.querySelector('#save-draft'); + const status = parentElement.querySelector('#status'); + + // Load draft if available + const draft = data?.draft || {}; + nameInput.value = draft.name || ''; + emailInput.value = draft.email || ''; + messageInput.value = draft.message || ''; + + // Update draft state as user types + const updateDraft = () => { + setStateValue('draft', { + name: nameInput.value, + email: emailInput.value, + message: messageInput.value + }); + }; + + // Save draft + const saveDraft = () => { + updateDraft(); + setTriggerValue('action', 'save_draft'); + status.textContent = 'Draft saved!'; + status.style.color = 'var(--st-green-color)'; + setTimeout(() => status.textContent = '', 2000); + }; + + // Submit form + const submitForm = (e) => { + e.preventDefault(); + + if (!nameInput.value || !emailInput.value || !messageInput.value) { + status.textContent = 'Please fill all fields'; + status.style.color = 'var(--st-red-color)'; + return; + } + + updateDraft(); + setTriggerValue('action', 'submit'); + status.textContent = 'Sending message...'; + status.style.color = 'var(--st-blue-color)'; + }; + + // Event listeners + nameInput.addEventListener('input', updateDraft); + emailInput.addEventListener('input', updateDraft); + messageInput.addEventListener('input', updateDraft); + saveDraftBtn.addEventListener('click', saveDraft); + form.addEventListener('submit', submitForm); + + // Initialize + updateDraft(); + } + """ +) + +# Handle form actions +def handle_form_action(): + if result.action == 'save_draft': + st.info("Draft saved!") + elif result.action == 'submit': + st.success("Message sent successfully!") + # Clear form after successful submission + st.rerun() + +# Use the component +result = form_component( + data={"draft": st.session_state.get("form_draft", {})}, + on_draft_change=lambda: setattr(st.session_state, "form_draft", result.draft), + on_action_change=handle_form_action +) + +# Show draft status +if result.draft and any(result.draft.values()): + st.write("**Current draft:**") + st.json(result.draft) +``` + +**Key concepts demonstrated:** + +- Form handling and validation +- Real-time state updates as user types +- Draft saving functionality +- Multiple action types with single callback +- Session state integration for persistence + +## What's next? + +Now that you've seen these examples: + +- Learn the fundamentals in [Create components](/develop/concepts/custom-components/components-v2/create). +- Understand [State vs triggers](/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/state-and-triggers.md b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md new file mode 100644 index 000000000..c2f0334d4 --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -0,0 +1,548 @@ +--- +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. +- 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)` | + +The callback pattern and value access are the same for both state and trigger values. + +## State values in practice + +State values are perfect for tracking the ongoing state of your component. Here's a practical example: + +```python +import streamlit as st + +# Create a component that tracks user preferences +preferences_component = st.components.v2.component( + name="user_preferences", + html=""" +
+

User Preferences

+ +
+ +
+ +
+ """, + css=""" + .preferences { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: 8px; + font-family: var(--st-font); + } + label { + display: block; + margin: 10px 0; + } + """, + js=""" + export default function({ parentElement, setStateValue, data }) { + const notifications = parentElement.querySelector('#notifications'); + const theme = parentElement.querySelector('#theme'); + const fontSize = parentElement.querySelector('#font-size'); + const fontDisplay = parentElement.querySelector('#font-display'); + + // Initialize with default values + const defaults = data?.defaults || {}; + notifications.checked = defaults.notifications || false; + theme.value = defaults.theme || 'light'; + fontSize.value = defaults.fontSize || 14; + fontDisplay.textContent = fontSize.value; + + // Update state when values change + const updateState = () => { + setStateValue('notifications', notifications.checked); + setStateValue('theme', theme.value); + setStateValue('fontSize', parseInt(fontSize.value)); + }; + + // Handle font size changes with display update + const handleFontSizeChange = () => { + fontDisplay.textContent = fontSize.value; + updateState(); + }; + + // Attach event listeners + notifications.addEventListener('change', updateState); + theme.addEventListener('change', updateState); + fontSize.addEventListener('input', handleFontSizeChange); + + // Initialize state + updateState(); + + // Cleanup + return () => { + notifications.removeEventListener('change', updateState); + theme.removeEventListener('change', updateState); + fontSize.removeEventListener('input', handleFontSizeChange); + }; + } + """ +) + +# Use the component with state tracking +def handle_preferences_change(): + # This callback runs whenever any preference changes + st.rerun() # Optional: force immediate UI update + +result = preferences_component( + key="user_prefs", + data={"defaults": {"notifications": True, "theme": "dark", "fontSize": 16}}, + default={"notifications": False, "theme": "light", "fontSize": 14}, + on_notifications_change=handle_preferences_change, + on_theme_change=handle_preferences_change, + on_fontSize_change=handle_preferences_change +) + +# Access persistent state values +st.write("**Current Preferences:**") +st.write(f"- Notifications: {result.notifications}") +st.write(f"- Theme: {result.theme}") +st.write(f"- Font Size: {result.fontSize}px") + +# Use the preferences to configure your app +if result.theme == "dark": + st.markdown("🌙 Dark theme selected") +else: + st.markdown("☀️ Light theme selected") +``` + +## Trigger values in practice + +Trigger values are ideal for handling discrete user actions. Here's an example: + +```python +import streamlit as st + +# Initialize session state for tracking actions +if "action_log" not in st.session_state: + st.session_state.action_log = [] +if "save_count" not in st.session_state: + st.session_state.save_count = 0 + +# Create a component with various action buttons +action_component = st.components.v2.component( + name="action_buttons", + html=""" +
+

Document Actions

+ + + + +
+ """, + css=""" + .actions { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: 8px; + } + button { + margin: 5px; + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + } + .primary { + background: var(--st-primary-color); + color: white; + } + .secondary { + background: var(--st-secondary-background-color); + color: var(--st-text-color); + border: 1px solid var(--st-border-color); + } + .danger { + background: var(--st-red-color); + color: white; + } + button:hover { + opacity: 0.8; + } + """, + js=""" + export default function({ parentElement, setTriggerValue }) { + const saveBtn = parentElement.querySelector('#save'); + const exportBtn = parentElement.querySelector('#export'); + const shareBtn = parentElement.querySelector('#share'); + const deleteBtn = parentElement.querySelector('#delete'); + + // Handle different actions with trigger values + const handleSave = () => { + setTriggerValue('action', 'save'); + }; + + const handleExport = () => { + setTriggerValue('action', 'export'); + }; + + const handleShare = () => { + setTriggerValue('action', 'share'); + }; + + const handleDelete = () => { + // Confirm before triggering delete + if (confirm('Are you sure you want to delete this document?')) { + setTriggerValue('action', 'delete'); + } + }; + + // Attach event listeners + saveBtn.addEventListener('click', handleSave); + exportBtn.addEventListener('click', handleExport); + shareBtn.addEventListener('click', handleShare); + deleteBtn.addEventListener('click', handleDelete); + + // Cleanup + return () => { + saveBtn.removeEventListener('click', handleSave); + exportBtn.removeEventListener('click', handleExport); + shareBtn.removeEventListener('click', handleShare); + deleteBtn.removeEventListener('click', handleDelete); + }; + } + """ +) + +# Define action handlers +def handle_action(): + action = result.action + timestamp = st.session_state.get('timestamp', 'Unknown time') + + if action == 'save': + st.session_state.save_count += 1 + st.session_state.action_log.append(f"Document saved (#{st.session_state.save_count})") + st.success("Document saved successfully!") + + elif action == 'export': + st.session_state.action_log.append("Document exported") + st.info("Document exported to downloads folder") + + elif action == 'share': + st.session_state.action_log.append("Share link generated") + st.info("Share link copied to clipboard!") + + elif action == 'delete': + st.session_state.action_log.append("Document deleted") + st.error("Document deleted permanently") + +# Use the component +result = action_component( + key="doc_actions", + on_action_change=handle_action +) + +# Show action feedback only when triggered +if result.action: + st.write(f"**Last action:** {result.action}") + +# Display action log +if st.session_state.action_log: + st.write("**Action History:**") + for i, log_entry in enumerate(reversed(st.session_state.action_log[-5:]), 1): + st.write(f"{i}. {log_entry}") +``` + +## Combining state and triggers + +Many components benefit from using both patterns together. Here's a comprehensive example: + +```python +import streamlit as st + +# Initialize session state +if "form_submissions" not in st.session_state: + st.session_state.form_submissions = 0 + +# Create a form component that uses both state and triggers +form_component = st.components.v2.component( + name="interactive_form", + html=""" +
+

Contact Form

+
+ + + +
+ + +
+
+
+
+ """, + css=""" + .form-container { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: 8px; + max-width: 500px; + } + input, textarea { + width: 100%; + padding: 10px; + margin: 5px 0; + border: 1px solid var(--st-border-color); + border-radius: 4px; + font-family: var(--st-font); + } + textarea { + height: 100px; + resize: vertical; + } + .form-actions { + display: flex; + gap: 10px; + margin-top: 10px; + } + button { + padding: 10px 15px; + border: none; + border-radius: 4px; + cursor: pointer; + } + #save-draft { + background: var(--st-secondary-background-color); + color: var(--st-text-color); + border: 1px solid var(--st-border-color); + } + #submit { + background: var(--st-primary-color); + color: white; + } + #status { + margin-top: 10px; + font-size: 14px; + } + """, + js=""" + export default function({ parentElement, setStateValue, setTriggerValue, data }) { + const form = parentElement.querySelector('#contact-form'); + const nameInput = parentElement.querySelector('#name'); + const emailInput = parentElement.querySelector('#email'); + const messageInput = parentElement.querySelector('#message'); + const saveDraftBtn = parentElement.querySelector('#save-draft'); + const submitBtn = parentElement.querySelector('#submit'); + const status = parentElement.querySelector('#status'); + + // Load draft data if available + const draft = data?.draft || {}; + nameInput.value = draft.name || ''; + emailInput.value = draft.email || ''; + messageInput.value = draft.message || ''; + + // Update state as user types (for draft saving) + const updateDraft = () => { + setStateValue('draft', { + name: nameInput.value, + email: emailInput.value, + message: messageInput.value + }); + }; + + // Save draft action (trigger) + const saveDraft = () => { + updateDraft(); + setTriggerValue('action', 'save_draft'); + status.textContent = '✅ Draft saved'; + setTimeout(() => status.textContent = '', 2000); + }; + + // Submit form action (trigger) + const submitForm = (e) => { + e.preventDefault(); + + // Validate form + if (!nameInput.value || !emailInput.value || !messageInput.value) { + status.textContent = '❌ Please fill all fields'; + return; + } + + updateDraft(); + setTriggerValue('action', 'submit'); + status.textContent = '📤 Sending message...'; + }; + + // Attach event listeners + nameInput.addEventListener('input', updateDraft); + emailInput.addEventListener('input', updateDraft); + messageInput.addEventListener('input', updateDraft); + saveDraftBtn.addEventListener('click', saveDraft); + form.addEventListener('submit', submitForm); + + // Initialize state + updateDraft(); + + // Cleanup + return () => { + nameInput.removeEventListener('input', updateDraft); + emailInput.removeEventListener('input', updateDraft); + messageInput.removeEventListener('input', updateDraft); + saveDraftBtn.removeEventListener('click', saveDraft); + form.removeEventListener('submit', submitForm); + }; + } + """ +) + +# Define action handlers +def handle_form_action(): + action = result.action + + if action == 'save_draft': + st.info("💾 Draft saved automatically") + + elif action == 'submit': + st.session_state.form_submissions += 1 + st.success(f"📤 Message sent successfully! (Submission #{st.session_state.form_submissions})") + # Clear the draft after successful submission + result.draft = {"name": "", "email": "", "message": ""} + +# Use the component +result = form_component( + key="contact_form", + data={"draft": st.session_state.get("form_draft", {})}, + default={"draft": {"name": "", "email": "", "message": ""}}, + on_draft_change=lambda: None, # Register draft as state + on_action_change=handle_form_action # Handle form actions +) + +# Store draft in session state (persistent across reruns) +if result.draft: + st.session_state.form_draft = result.draft + +# Show current draft status +if result.draft and any(result.draft.values()): + st.write("**Current Draft:**") + if result.draft.get('name'): + st.write(f"- Name: {result.draft['name']}") + if result.draft.get('email'): + st.write(f"- Email: {result.draft['email']}") + if result.draft.get('message'): + st.write(f"- Message: {len(result.draft['message'])} characters") +``` + +## 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 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/v2/theming) to make your components look great. +- Explore [Package-based components](/develop/concepts/custom-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..1231c5c2f --- /dev/null +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -0,0 +1,554 @@ +--- +title: 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 +--- + +# 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 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). + + + +| 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-heading-font-sizes` | `theme.headingFontSizes` | +| `--st-heading-font-weights` | `theme.headingFontWeights` | +| `--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-chart-categorical-colors` | `theme.chartCategoricalColors` | +| `--st-chart-sequential-colors` | `theme.chartSequentialColors` | +| `--st-heading-color` | `theme.headingColor` | +| `--st-border-color-light` | `theme.borderColorLight` | +| `--st-code-text-color` | `theme.codeTextColor` | +| `--st-widget-border-color` | `theme.widgetBorderColor` | +| `--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` | + + + + + +Array properties like `--st-heading-font-sizes`, `--st-chart-categorical-colors`, and `--st-chart-sequential-colors` are exposed as comma-separated strings. You can parse these in JavaScript if needed for dynamic styling. + + + +## Practical theming examples + +### Basic themed component + +Here's a simple component that uses Streamlit's theming: + +```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: 20px; + margin: 10px 0; + font-family: var(--st-font); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + } + + .card-title { + color: var(--st-heading-color); + font-family: var(--st-heading-font); + font-size: 1.2em; + margin: 0 0 10px 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: 8px 16px; + cursor: pointer; + font-family: var(--st-font); + font-size: var(--st-base-font-size); + 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 + +A component that uses Streamlit's status colors with a dynamically set status type and message: + +```python +import streamlit as st + +status_component = st.components.v2.component( + name="status_message", + html=""" +
+ + +
+ """, + css=""" + .status { + display: flex; + align-items: center; + padding: 12px 16px; + margin: 8px 0; + border-radius: var(--st-base-radius); + border-left: 4px 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: 10px; + font-size: 16px; + } + + .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 + +A component that matches 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: 12px 16px; + text-align: left; + border-bottom: 1px solid var(--st-dataframe-border-color); + font-size: var(--st-base-font-size); + } + + .data-table td { + padding: 12px 16px; + 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: 4px 8px; + border-radius: calc(var(--st-base-radius) / 2); + font-size: 0.85em; + 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 + +Set `isolate_styles=False` to allow style inheritance: + +```python +# Styles can inherit and 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 +) +``` + +Use cases for non-isolation: + +- When you want to inherit Streamlit's base styles instead of setting them explicitly through CSS custom properties. +- For components that need to integrate closely with the page. +- When building components that extend existing Streamlit elements. + +## 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 creates a responsive grid layout that adapts to the screen size: + +```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: 16px; + padding: 16px; + 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: 20px; + text-align: center; + color: var(--st-text-color); + transition: transform 0.2s; + } + + .grid-item:hover { + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + } + + /* Mobile-specific styles */ + @media (max-width: 768px) { + .responsive-grid { + grid-template-columns: 1fr; + gap: 12px; + padding: 12px; + } + + .grid-item { + padding: 16px; + } + } + """ +) + +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); +} +``` + +### Provide fallback values + +Use fallback values for better compatibility: + +```css +.my-component { + color: var(--st-text-color, #262730); + font-family: var(--st-font, "Helvetica Neue", sans-serif); +} +``` + +### Test in both light and dark modes + +Always test your components in both theme modes to ensure proper contrast and readability. + +### Use semantic color names + +Choose colors based on their semantic meaning: + +```css +/* Good - semantic usage */ +.error-message { + color: var(--st-red-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/v2/package-based) for advanced development workflows. +- Learn about [State vs triggers](/develop/concepts/custom-components/v2/state-and-triggers) for interactive components. +- Check out the [Create components](/develop/concepts/custom-components/v2/create) guide 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..6b912c483 --- /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 optimatized 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..474d09d07 100644 --- a/content/menu.md +++ b/content/menu.md @@ -98,14 +98,30 @@ 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 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 / 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/quickstart + - category: Develop / Concepts / Custom components / Components v2 / Create components + url: /develop/concepts/custom-components/components-v2/create + - 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 / 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 / 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 From 4890e2bfcef1dad01c5b422b62d549cfc8b6d1f5 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 5 Dec 2025 12:03:58 -0800 Subject: [PATCH 02/26] Update first three quickstart examples. --- .../custom-components/components-v2/create.md | 2 +- .../components-v2/quickstart.md | 349 +++++++++++++++--- 2 files changed, 305 insertions(+), 46 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index cd05c44e1..7b60b4c1b 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -5,7 +5,7 @@ description: Learn how to create Streamlit custom components v2 with inline deve keywords: custom components v2, create components, inline components, component registration, component mounting, bidirectional communication --- -# Create custom v2 component +# Create custom v2 components Components v2 provides a modern, flexible approach to extending Streamlit with custom functionality. This guide will walk you through creating your first component using the inline development approach. For package-based components, see the [Package-based Components](/develop/concepts/custom-components/components-v2/package-based) guide. diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index a22463196..ad1a4912d 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -7,7 +7,7 @@ keywords: custom components v2, quickstart, examples, interactive components, da # Quickstart examples -Get started with Custom Components v2 through these practical examples that demonstrate the key features and capabilities. +Get started with custom components v2 through these practical examples that demonstrate the key features and capabilities. ## Two-step component process @@ -16,15 +16,21 @@ Creating and using a custom component involves two distinct steps: 1. Register your component to define its structure (HTML, CSS, JavaScript). - Register a component with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). - Within your component's JavaScript function, communicate with Python by destructuring a [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) object. + - Within your component's CSS, use Streamlit's [theme variables](/develop/concepts/custom-components/components-v2/theming#using-css-custom-properties) to style your component. 2. Mount your component to create a specific instance in your app. - - Use your component command that implements the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) type. + - Use your component command, which inherits from the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) class. -For more information, see [Create components](/develop/concepts/custom-components/components-v2/create). +For more information, see [Create custom v2 components](/develop/concepts/custom-components/components-v2/create). ## Simple interactive button -This example shows the basics of creating an interactive component with bidirectional communication: +This example shows the basics of creating an interactive component with bidirectional communication. It shows the following key concepts: + +- Component registration with HTML, CSS, and JavaScript. +- Trigger values using `setTriggerValue()`. +- Callback functions with the `on__change` naming pattern. +- Mounting a component with its command created from registration. ```python import streamlit as st @@ -38,6 +44,15 @@ def handle_button_click(): my_component = st.components.v2.component( "interactive_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; @@ -55,29 +70,165 @@ if result.action: st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") ``` -**Key concepts demonstrated:** +You can pass HTML, CSS, and JavaScript code to your component through file references. The previous example is equivalent to the following: + +``` +project_directory/ +├── my_component/ +│ ├── my_css.css +│ ├── my_html.html +│ └── my_js.js +└── streamlit_app.py +``` + + + +```css +button { + border: none; + padding: 0.5rem; + border-radius: var(--st-button-radius); + background-color: var(--st-primary-color); + color: white; +} +``` -- Component registration with HTML and JavaScript -- Trigger values using `setTriggerValue()` -- Callback functions with `on_{event}_change` pattern -- Session state integration + + + + +```markup + +``` + + + + + +```javascript +export default function (component) { + const { setTriggerValue, parentElement } = component; + + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); + }; +} +``` + + + + + +```python +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( + "interactive_button", + html="./my_component/my_html.html", + css="./my_component/my_css.css", + js="./my_component/my_js.js", +) + +result = my_component(on_action_change=handle_button_click) + +if result.action: + st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") +``` + + + +The remaining examples on this page will use this file structure for easier viewing of the embedded code blocks. The complete code is provided at the end of each example for easier copying and pasting. ## Rich data exchange -This example shows how to pass different data types to your component: +This example shows how to pass different data types to your component. It shows the following key concepts: + +- Automatic dataframe conversion to Arrow format. +- Passing JSON data. +- Passing an image as a base64 string. +- Accessing data in JavaScript via the destructured `data` property. + +`my_component/my_html.html`: + +```markup +
Loading data...
+``` + +`my_component/my_js.js`: + +```javascript +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}

+ + `; +} +``` + +`streamlit_app.py`: + +```python +import pandas as pd +import streamlit as st +import base64 + +# Create sample data +df = pd.DataFrame({ + "name": ["Alice", "Bob", "Charlie"], + "city": ["New York", "London", "Tokyo"] +}) +# Load an image and convert to bytes +with open("favi.png", "rb") as img_file: + img_bytes = img_file.read() +img_base64 = base64.b64encode(img_bytes).decode('utf-8') + +# Serialization is automatically handled by Streamlit components +chart_component = st.components.v2.component( + "data_display", + html="./my_component/my_html.html", + js="./my_component/my_js.js", +) + +result = chart_component( + data={ + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64 # Image as base64 string + } +) +``` + + ```python import pandas as pd import streamlit as st +import base64 # Create sample data df = pd.DataFrame({ "name": ["Alice", "Bob", "Charlie"], - "age": [25, 30, 35], "city": ["New York", "London", "Tokyo"] }) +# Load an image and convert to bytes +with open("favi.png", "rb") as img_file: + img_bytes = img_file.read() +img_base64 = base64.b64encode(img_bytes).decode('utf-8') -# Pass multiple DataFrames, JSON, or raw bytes - automatically handled +# Serialization is automatically handled by Streamlit components chart_component = st.components.v2.component( "data_display", html="
Loading data...
", @@ -85,40 +236,160 @@ chart_component = st.components.v2.component( export default function({ data, parentElement }) { const container = parentElement.querySelector('#data-container'); - // Access different data types + const df = data.df; const userInfo = data.user_info; - const dataframe = data.df; // Automatically converted from pandas - const settings = data.settings; + const imgBase64 = data.image_base64; container.innerHTML = ` -

User: ${userInfo.name}

-

DataFrame rows: ${dataframe.length}

-

Theme: ${settings.theme}

+

Dataframe: ${df}

+

User Info: ${userInfo.name}

+ `; } - """ + """, ) result = chart_component( data={ - "df": df, # Converted to Arrow format - "user_info": {"name": "Alice"}, # Passed as JSON - "settings": {"theme": "dark"}, # Passed as JSON - "binary_data": b"raw bytes" # Binary data support + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64 # Image as base64 string } ) ``` -**Key concepts demonstrated:** - -- Automatic dataframe conversion to Arrow format -- JSON data passing -- Binary data support -- Accessing data in JavaScript via `component.data` +
## Complete interactive counter -This comprehensive example demonstrates both state and trigger values: +This comprehensive example demonstrates both state and trigger values. It shows the following key concepts: + +- Using state and trigger values together in one component. +- Using CSS custom properties to style the component. +- Bidirectional communication between Python and JavaScript. +- Multiple event handlers. +- Cleanup functions for proper resource management + +`my_component/my_html.html`: + +```markup +
+

Count: 0

+
+ + + +
+
+``` + +`my_component/my_css.css`: + +```css +.counter { + padding: 20px; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + text-align: center; +} +.buttons { + margin-top: 15px; +} +button { + margin: 0 5px; + padding: 8px 16px; + 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); +} +``` + +`my_component/my_js.js`: + +```javascript +export default function ({ + parentElement, + setStateValue, + setTriggerValue, + data, +}) { + let count = data?.initialCount || 0; + const display = parentElement.querySelector("#display"); + const incrementBtn = parentElement.querySelector("#increment"); + const decrementBtn = parentElement.querySelector("#decrement"); + const resetBtn = parentElement.querySelector("#reset"); + + const updateDisplay = () => { + display.textContent = count; + setStateValue("count", count); // Persistent state + }; + + incrementBtn.onclick = () => { + count++; + updateDisplay(); + }; + + decrementBtn.onclick = () => { + count--; + updateDisplay(); + }; + + resetBtn.onclick = () => { + count = 0; + updateDisplay(); + setTriggerValue("reset", true); // One-time trigger + }; + + // Initialize + updateDisplay(); + + // Cleanup function + return () => { + incrementBtn.removeEventListener("click", incrementBtn.onclick); + decrementBtn.removeEventListener("click", decrementBtn.onclick); + resetBtn.removeEventListener("click", resetBtn.onclick); + }; +} +``` + +`streamlit_app.py`: + +```python +import streamlit as st + +# Interactive counter with both state and triggers +counter = st.components.v2.component( + "interactive_counter", + html="my_component/my_html.html", + css="my_component/my_css.css", + js="my_component/my_js.js", +) + +# Use with callbacks +result = counter( + 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!") +``` + + ```python import streamlit as st @@ -205,16 +476,11 @@ counter = st.components.v2.component( """ ) -# Define callbacks -def handle_reset(): - st.balloons() - st.success("Counter reset!") - # Use with callbacks result = counter( data={"initialCount": 0}, on_count_change=lambda: None, # Track count state - on_reset_change=handle_reset # Handle reset events + on_reset_change=lambda: None, # Handle reset events ) # Display current state @@ -222,17 +488,10 @@ st.write(f"Current count: {result.count}") # Show when reset was triggered (only for one rerun) if result.reset: - st.info("Reset button was clicked!") + st.toast("Counter was reset!") ``` -**Key concepts demonstrated:** - -- Both state values (`setStateValue`) and trigger values (`setTriggerValue`) -- Theme integration with CSS custom properties -- Data passing from Python to JavaScript -- Multiple event handlers and callbacks -- Cleanup functions for proper resource management -- Difference between persistent state and transient triggers + ## Form with validation From 77e02d40216ebe1e10c5d59e1ad1188e6b01a98b Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 5 Dec 2025 14:08:41 -0800 Subject: [PATCH 03/26] Edits --- .../concepts/custom-components/_index.md | 12 +++--- .../components-v2/quickstart.md | 37 ++++++++++++++----- .../components-v2/state-and-triggers.md | 1 + .../components-v2/theming.md | 27 +++----------- content/menu.md | 16 ++++---- 5 files changed, 48 insertions(+), 45 deletions(-) diff --git a/content/develop/concepts/custom-components/_index.md b/content/develop/concepts/custom-components/_index.md index 770b95ad9..0585ba456 100644 --- a/content/develop/concepts/custom-components/_index.md +++ b/content/develop/concepts/custom-components/_index.md @@ -21,19 +21,19 @@ Learn what custom components are, when to use them, and understand the differenc - + -

Components v1

+

Components v2

-The original custom components framework. Learn how to use and build v1 components. +The next generation of custom components with enhanced capabilities, bidirectional communication, and simplified development.
- + -

Components v2

+

Components v1

-The next generation of custom components with enhanced capabilities, bidirectional communication, and simplified development. +The original custom components framework. Learn how to use and build v1 components.
diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index ad1a4912d..19e754eb6 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -70,7 +70,7 @@ if result.action: st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") ``` -You can pass HTML, CSS, and JavaScript code to your component through file references. The previous example is equivalent to the following: +For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you'll need to read them into strings. The previous example is equivalent to the following: ``` project_directory/ @@ -122,6 +122,13 @@ export default function (component) { ```python import streamlit as st +with open("my_component/my_html.html", "r") as f: + HTML = f.read() +with open("my_component/my_css.css", "r") as f: + CSS = f.read() +with open("my_component/my_js.js", "r") as f: + JS = f.read() + if "click_count" not in st.session_state: st.session_state.click_count = 0 @@ -130,9 +137,9 @@ def handle_button_click(): my_component = st.components.v2.component( "interactive_button", - html="./my_component/my_html.html", - css="./my_component/my_css.css", - js="./my_component/my_js.js", + html=HTML, + css=CSS, + js=JS, ) result = my_component(on_action_change=handle_button_click) @@ -185,6 +192,11 @@ import pandas as pd import streamlit as st import base64 +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() + # Create sample data df = pd.DataFrame({ "name": ["Alice", "Bob", "Charlie"], @@ -198,8 +210,8 @@ img_base64 = base64.b64encode(img_bytes).decode('utf-8') # Serialization is automatically handled by Streamlit components chart_component = st.components.v2.component( "data_display", - html="./my_component/my_html.html", - js="./my_component/my_js.js", + html=HTML, + js=JS, ) result = chart_component( @@ -366,12 +378,19 @@ export default function ({ ```python import streamlit as st +with open("my_component/my_html.html", "r") as f: + HTML = f.read() +with open("my_component/my_css.css", "r") as f: + CSS = f.read() +with open("my_component/my_js.js", "r") as f: + JS = f.read() + # Interactive counter with both state and triggers counter = st.components.v2.component( "interactive_counter", - html="my_component/my_html.html", - css="my_component/my_css.css", - js="my_component/my_js.js", + html=HTML, + css=CSS, + js=JS, ) # Use with callbacks 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 index c2f0334d4..4590ad767 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -33,6 +33,7 @@ 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 diff --git a/content/develop/concepts/custom-components/components-v2/theming.md b/content/develop/concepts/custom-components/components-v2/theming.md index 1231c5c2f..38e41b5e9 100644 --- a/content/develop/concepts/custom-components/components-v2/theming.md +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -7,7 +7,7 @@ keywords: custom components v2, theming, CSS custom properties, styling, theme i # 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 that expose Streamlit's theme values directly to your component styles. +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 @@ -413,10 +413,10 @@ Benefits of isolation: ### Non-isolated styles -Set `isolate_styles=False` to allow style inheritance: +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 inherit and affect the page +# Styles can affect the page non_isolated_component = st.components.v2.component( name="non_isolated", html="
Content with inheritance
", @@ -425,12 +425,6 @@ non_isolated_component = st.components.v2.component( ) ``` -Use cases for non-isolation: - -- When you want to inherit Streamlit's base styles instead of setting them explicitly through CSS custom properties. -- For components that need to integrate closely with the page. -- When building components that extend existing Streamlit elements. - ## 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 creates a responsive grid layout that adapts to the screen size: @@ -510,20 +504,9 @@ Instead of hardcoding colors, always use Streamlit's theme variables: } ``` -### Provide fallback values - -Use fallback values for better compatibility: - -```css -.my-component { - color: var(--st-text-color, #262730); - font-family: var(--st-font, "Helvetica Neue", sans-serif); -} -``` - -### Test in both light and dark modes +### Test in different themes -Always test your components in both theme modes to ensure proper contrast and readability. +Always test your components in both light and dark base themes. Preferably, test your component with a custom theme as well. ### Use semantic color names diff --git a/content/menu.md b/content/menu.md index 474d09d07..d3540958e 100644 --- a/content/menu.md +++ b/content/menu.md @@ -100,14 +100,6 @@ site_menu: url: /develop/concepts/custom-components - category: Develop / Concepts / Custom components / Overview url: /develop/concepts/custom-components/overview - - 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 / Components v2 url: /develop/concepts/custom-components/components-v2 - category: Develop / Concepts / Custom components / Components v2 / Quickstart examples @@ -120,6 +112,14 @@ site_menu: 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 / Component gallery From f829c7d1d913c82135d706a06acf7717ad2d2832 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 5 Dec 2025 22:56:24 -0800 Subject: [PATCH 04/26] Validate final quickstart example. --- .../components-v2/quickstart.md | 360 +++++++++++++++--- 1 file changed, 300 insertions(+), 60 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 19e754eb6..f1d1b7d05 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -15,8 +15,8 @@ Creating and using a custom component involves two distinct steps: 1. Register your component to define its structure (HTML, CSS, JavaScript). - Register a component with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). - - Within your component's JavaScript function, communicate with Python by destructuring a [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) object. - - Within your component's CSS, use Streamlit's [theme variables](/develop/concepts/custom-components/components-v2/theming#using-css-custom-properties) to style your component. + - Optional: To enable bidirectional communication, within your component's JavaScript function, communicate with Python by calling `setStateValue()` or `setTriggerValue()`. These are properties of the [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) object passed to your function. + - Optional: To make your component theme-aware, within your component's CSS, style your component with Streamlit's [theme variables](/develop/concepts/custom-components/components-v2/theming#using-css-custom-properties). 2. Mount your component to create a specific instance in your app. - Use your component command, which inherits from the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) class. @@ -514,7 +514,226 @@ if result.reset: ## Form with validation -This example shows a more complex component with form validation: +This example shows a more complex component with form validation. It shows the following key concepts: + +- Form handling and validation. +- Draft saving functionality. +- Multiple event handlers and callbacks. +- Using CSS custom properties to style the component. +- Session state integration for more complex, bidirectional state management. +- Cleanup functions for proper resource management. + +`my_component/my_html.html`: + +```markup +
+

Contact Form

+
+ + + +
+ + +
+
+
+
+``` + +`my_component/my_css.css`: + +```css +.form-container { + padding: 1rem; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + box-sizing: border-box; +} +h3 { + font-size: var(--st-heading-font-size-h3, inherit); + font-weight: var(--st-heading-font-weight-h3, inherit); + margin: 0; +} +input, +textarea { + width: 100%; + padding: 0.5rem; + margin: 0.5rem 0; + background: var(--st-secondary-background-color); + border: 1px solid transparent; + border-radius: var(--st-base-radius); + box-sizing: border-box; + font-size: inherit; + font-family: inherit; +} +input:focus, +textarea:focus { + outline: none; + border-color: var(--st-primary-color); +} +textarea { + height: 5rem; + resize: vertical; +} +.form-actions { + display: flex; + gap: 1rem; + margin-top: 0.75rem; +} +button { + padding: 0.5rem 1rem; + border-radius: var(--st-button-radius); + border: 1px solid transparent; + font-size: inherit; + font-family: inherit; +} +button[type="submit"] { + background: var(--st-primary-color); + color: white; +} +button[type="button"] { + border: 1px solid var(--st-border-color); + background: var(--st-primary-background-color); + color: var(--st-text-color); +} +button:hover { + opacity: 0.9; + border-color: var(--st-primary-color); +} +#status { + margin-top: 0.5rem; +} +``` + +`my_component/my_js.js`: + +```javascript +export default function ({ + parentElement, + setStateValue, + setTriggerValue, + data, +}) { + const form = parentElement.querySelector("#contact-form"); + const h3 = parentElement.querySelector("h3"); + const nameInput = parentElement.querySelector("#name"); + const emailInput = parentElement.querySelector("#email"); + const messageInput = parentElement.querySelector("#message"); + const saveDraftBtn = parentElement.querySelector("#save-draft"); + const status = parentElement.querySelector("#status"); + + // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights + requestAnimationFrame(() => { + const container = parentElement.querySelector(".form-container"); + const headingSizes = getComputedStyle(form) + .getPropertyValue("--st-heading-font-sizes") + .trim(); + const headingWeights = getComputedStyle(form) + .getPropertyValue("--st-heading-font-weights") + .trim(); + const sizes = headingSizes.split(",").map((s) => s.trim()); + const weights = headingWeights.split(",").map((s) => s.trim()); + if (sizes[2] && container) { + container.style.setProperty("--st-heading-font-size-h3", sizes[2]); + } + if (weights[2] && container) { + container.style.setProperty("--st-heading-font-weight-h3", weights[2]); + } + }); + + // Load draft if available + const draft = data?.draft || {}; + nameInput.value = draft.name || ""; + emailInput.value = draft.email || ""; + messageInput.value = draft.message || ""; + + // Save draft + const saveDraft = () => { + setStateValue("draft", { + name: nameInput.value, + email: emailInput.value, + message: messageInput.value, + }); + setTriggerValue("action", "save_draft"); + status.textContent = "Draft saved!"; + status.style.color = "var(--st-green-color)"; + setTimeout(() => (status.textContent = ""), 2000); + }; + + // Submit form + const submitForm = (e) => { + e.preventDefault(); + + if (!nameInput.value || !emailInput.value || !messageInput.value) { + status.textContent = "Please fill all fields"; + status.style.color = "var(--st-red-color)"; + return; + } + + status.textContent = "Message sent!"; + status.style.color = "var(--st-blue-color)"; + setTimeout(() => (status.textContent = ""), 2000); + setTriggerValue("submit", { + name: nameInput.value, + email: emailInput.value, + message: messageInput.value, + }); + }; + + // Event listeners - only update on button clicks + saveDraftBtn.addEventListener("click", saveDraft); + form.addEventListener("submit", submitForm); + + return () => { + saveDraftBtn.removeEventListener("click", saveDraft); + form.removeEventListener("submit", submitForm); + }; +} +``` + +`streamlit_app.py`: + +```python +import streamlit as st + +with open("my_component/my_html.html", "r") as f: + HTML = f.read() +with open("my_component/my_css.css", "r") as f: + CSS = f.read() +with open("my_component/my_js.js", "r") as f: + JS = f.read() + +form_component = st.components.v2.component( + "contact_form", + html=HTML, + css=CSS, + js=JS, +) + +# Handle form actions +def handle_form_action(): + # Clear the draft on submit + st.session_state.message_form.draft={} + +# Use the component +form_state = st.session_state.get("message_form", {}) +result = form_component( + data={"draft": form_state.get("draft", {})}, + on_draft_change=lambda: None, + on_submit_change=handle_form_action, + key="message_form" +) + +if result.submit: + st.write("Message Submitted:") + result.submit +else: + st.write("Current Draft:") + result.draft +``` + + ```python import streamlit as st @@ -531,85 +750,109 @@ form_component = st.components.v2.component(
+
-
""", css=""" .form-container { - padding: 20px; + padding: 1rem;; border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); - font-family: var(--st-font); - max-width: 500px; + box-sizing: border-box; + } + h3 { + font-size: var(--st-heading-font-size-h3, inherit); + font-weight: var(--st-heading-font-weight-h3, inherit); + margin: 0; } input, textarea { width: 100%; - padding: 10px; - margin: 10px 0; - border: 1px solid var(--st-border-color); - border-radius: 4px; - font-family: var(--st-font); + padding: .5rem; + margin: .5rem 0; + background: var(--st-secondary-background-color); + border: 1px solid transparent; + border-radius: var(--st-base-radius); box-sizing: border-box; + font-size: inherit; + font-family: inherit; } + input:focus, textarea:focus { + outline: none; + border-color: var(--st-primary-color); + }; textarea { - height: 100px; + height: 5rem; resize: vertical; } .form-actions { display: flex; - gap: 10px; - margin-top: 15px; + gap: 1rem; + margin-top: .75rem; } button { - padding: 10px 20px; - border: none; - border-radius: 4px; - cursor: pointer; - font-family: var(--st-font); + padding: .5rem 1rem;; + border-radius: var(--st-button-radius); + border: 1px solid transparent; + font-size: inherit; + font-family: inherit; } button[type="submit"] { background: var(--st-primary-color); color: white; } button[type="button"] { - background: var(--st-secondary-background-color); - color: var(--st-text-color); border: 1px solid var(--st-border-color); + background: var(--st-primary-background-color); + color: var(--st-text-color); + } + button:hover { + opacity: 0.9; + border-color: var(--st-primary-color); } #status { - margin-top: 10px; - font-size: 14px; + margin-top: .5rem; } """, js=""" export default function({ parentElement, setStateValue, setTriggerValue, data }) { const form = parentElement.querySelector('#contact-form'); + const h3 = parentElement.querySelector('h3'); const nameInput = parentElement.querySelector('#name'); const emailInput = parentElement.querySelector('#email'); const messageInput = parentElement.querySelector('#message'); const saveDraftBtn = parentElement.querySelector('#save-draft'); const status = parentElement.querySelector('#status'); + // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights + requestAnimationFrame(() => { + const container = parentElement.querySelector('.form-container'); + const headingSizes = getComputedStyle(form).getPropertyValue('--st-heading-font-sizes').trim(); + const headingWeights = getComputedStyle(form).getPropertyValue('--st-heading-font-weights').trim(); + const sizes = headingSizes.split(',').map(s => s.trim()); + const weights = headingWeights.split(',').map(s => s.trim()); + if (sizes[2] && container) { + container.style.setProperty('--st-heading-font-size-h3', sizes[2]); + } + if (weights[2] && container) { + container.style.setProperty('--st-heading-font-weight-h3', weights[2]); + } + }); + // Load draft if available const draft = data?.draft || {}; nameInput.value = draft.name || ''; emailInput.value = draft.email || ''; messageInput.value = draft.message || ''; - // Update draft state as user types - const updateDraft = () => { + // Save draft + const saveDraft = () => { setStateValue('draft', { name: nameInput.value, email: emailInput.value, message: messageInput.value }); - }; - - // Save draft - const saveDraft = () => { - updateDraft(); setTriggerValue('action', 'save_draft'); status.textContent = 'Draft saved!'; status.style.color = 'var(--st-green-color)'; @@ -626,54 +869,51 @@ form_component = st.components.v2.component( return; } - updateDraft(); - setTriggerValue('action', 'submit'); - status.textContent = 'Sending message...'; + status.textContent = 'Message sent!'; status.style.color = 'var(--st-blue-color)'; + setTimeout(() => status.textContent = '', 2000); + setTriggerValue('submit', { + name: nameInput.value, + email: emailInput.value, + message: messageInput.value + }); }; - // Event listeners - nameInput.addEventListener('input', updateDraft); - emailInput.addEventListener('input', updateDraft); - messageInput.addEventListener('input', updateDraft); + // Event listeners - only update on button clicks saveDraftBtn.addEventListener('click', saveDraft); form.addEventListener('submit', submitForm); - // Initialize - updateDraft(); + return () => { + saveDraftBtn.removeEventListener('click', saveDraft); + form.removeEventListener('submit', submitForm); + }; } """ ) # Handle form actions def handle_form_action(): - if result.action == 'save_draft': - st.info("Draft saved!") - elif result.action == 'submit': - st.success("Message sent successfully!") - # Clear form after successful submission - st.rerun() + # Clear the draft on submit + st.session_state.message_form.draft={} # Use the component +form_state = st.session_state.get("message_form", {}) result = form_component( - data={"draft": st.session_state.get("form_draft", {})}, - on_draft_change=lambda: setattr(st.session_state, "form_draft", result.draft), - on_action_change=handle_form_action + data={"draft": form_state.get("draft", {})}, + on_draft_change=lambda: None, + on_submit_change=handle_form_action, + key="message_form" ) -# Show draft status -if result.draft and any(result.draft.values()): - st.write("**Current draft:**") - st.json(result.draft) +if result.submit: + st.write("Message Submitted:") + result.submit +else: + st.write("Current Draft:") + result.draft ``` -**Key concepts demonstrated:** - -- Form handling and validation -- Real-time state updates as user types -- Draft saving functionality -- Multiple action types with single callback -- Session state integration for persistence + ## What's next? From ff874332cd2f913a56b6dd47db58bfb9ca493eea Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 5 Dec 2025 23:19:20 -0800 Subject: [PATCH 05/26] Correct file handling for inline components --- .../custom-components/components-v2/create.md | 35 ++++++++++++------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 7b60b4c1b..08ae884ab 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -43,9 +43,11 @@ my_component = st.components.v2.component( ### Registration parameters - The `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. -- The `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. It can be inline HTML code or a path to an `.html` file. -- The `css` (optional) is the CSS styling for your component. It can be inline CSS code or a path to a `.css` file. -- The `js` (optional) is the JavaScript logic for your component. It can be inline JavaScript code or a path to a `.js` file. +- The `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. +- The `css` (optional) is the CSS styling for your component. +- The `js` (optional) is the JavaScript logic for your component. + +For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. File references are only supported for package-based components. When you create a package-based component, Streamlit serves the contents of the package's asset directory, making those resources available to your app's frontend. When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend, relative to the served asset directory. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). @@ -88,7 +90,7 @@ export default function (component) { -Don't directly modify the inner HTML of the `parentElement`. This will delete the HTML, CSS, and JavaScript code that was registered with the component. If you need to inject content from `data`, create a placeholder element within `html` and update its content with JavaScript. +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`, either create a placeholder element within `html` to update or inject new elements into `parentElement`. @@ -106,26 +108,33 @@ hello_component = st.components.v2.component( ) ``` -#### File-based component +#### Using files for inline components -For larger components, you can organize your code into separate files: +For larger components, you can organize your code into separate files. However, for inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Similar to the examples shown in the [Quickstart](/develop/concepts/custom-components/components-v2/quickstart) guide, you can read the files into strings and pass them to your inline component. For package-based components, you can pass file references instead. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). ``` my_app/ ├── streamlit_app.py # Entrypoint file -├── styles/ -│ └── component.css # Component styles -└── scripts/ +└── frontend/ + ├── component.css # Component styles + ├── component.html # Component HTML └── component.js # Component JavaScript ``` ```python -# Load CSS and JS from external files +# Load HTML, CSS, and JS from external files +with open("frontend/component.css", "r") as f: + CSS = f.read() +with open("frontend/component.html", "r") as f: + HTML = f.read() +with open("frontend/component.js", "r") as f: + JS = f.read() + file_component = st.components.v2.component( name="file_based", - html="
Loading...
", - css="./styles/component.css", # Path to CSS file - js="./scripts/component.js" # Path to JS file + html=HTML, + css=CSS, + js=JS, ) ``` From 613215b58cf1fb900f2ea7369d957789d91b9ed2 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Fri, 5 Dec 2025 23:30:24 -0800 Subject: [PATCH 06/26] Edits --- .../concepts/custom-components/components-v2/create.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 08ae884ab..2d314a3b6 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -178,7 +178,7 @@ After registration, you mount your component in your Streamlit app. This creates result = my_component( key="unique_instance", data={"initial_value": 42}, - on_clicked_change=lambda: st.write("Button was clicked!") + on_clicked_change=lambda: None) ) ``` @@ -250,7 +250,7 @@ DataFrames are automatically serialized using Apache Arrow format, which provide #### 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 wrap your component in a `
` element that behaves like other Streamlit elements, but you are responsible for ensuring that the content within your component is responsive to the surrounding `
`. +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 throughout 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( @@ -259,6 +259,8 @@ result = my_component( ) ``` +On the frontend, it's generally recommended to set your component's CSS to `width: 100%; height: 100%`, since Streamlit will size the `
` wrapper element correctly. 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. @@ -351,7 +353,6 @@ counter_component = st.components.v2.component( border: none; border-radius: var(--st-button-radius); padding: 4px 8px; - cursor: pointer; font-family: var(--st-font); font-size: var(--st-base-font-size); } From 5aedeffac2996ddb0aab69c6add9dbe5dc45ef98 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 6 Dec 2025 10:50:18 -0800 Subject: [PATCH 07/26] Use caching --- .../components-v2/quickstart.md | 98 ++++++++++++------- 1 file changed, 60 insertions(+), 38 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index f1d1b7d05..152a8ad8f 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -75,12 +75,44 @@ For inline component development, you must pass raw HTML, CSS, and JavaScript co ``` project_directory/ ├── my_component/ +│ ├── __init__.py │ ├── my_css.css │ ├── my_html.html │ └── my_js.js └── streamlit_app.py ``` + + +```python +import streamlit as st +from pathlib import Path + +# Get the current file's directory +_COMPONENT_DIR = Path(__file__).parent + +@st.cache_data +def load_html(): + with open(_COMPONENT_DIR / "my_html.html", "r") as f: + return f.read() + +@st.cache_data +def load_css(): + with open(_COMPONENT_DIR / "my_css.css", "r") as f: + return f.read() + +@st.cache_data +def load_js(): + with open(_COMPONENT_DIR / "my_js.js", "r") as f: + return f.read() + +HTML = load_html() +CSS = load_css() +JS = load_js() +``` + + + ```css @@ -121,13 +153,7 @@ export default function (component) { ```python import streamlit as st - -with open("my_component/my_html.html", "r") as f: - HTML = f.read() -with open("my_component/my_css.css", "r") as f: - CSS = f.read() -with open("my_component/my_js.js", "r") as f: - JS = f.read() +from my_component import HTML, CSS, JS if "click_count" not in st.session_state: st.session_state.click_count = 0 @@ -191,21 +217,23 @@ export default function ({ data, parentElement }) { import pandas as pd import streamlit as st import base64 - -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() +from my_component import HTML, JS # Create sample data -df = pd.DataFrame({ - "name": ["Alice", "Bob", "Charlie"], - "city": ["New York", "London", "Tokyo"] +@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 bytes -with open("favi.png", "rb") as img_file: - img_bytes = img_file.read() -img_base64 = base64.b64encode(img_bytes).decode('utf-8') +@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 chart_component = st.components.v2.component( @@ -231,14 +259,20 @@ import streamlit as st import base64 # Create sample data -df = pd.DataFrame({ - "name": ["Alice", "Bob", "Charlie"], - "city": ["New York", "London", "Tokyo"] +@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 bytes -with open("favi.png", "rb") as img_file: - img_bytes = img_file.read() -img_base64 = base64.b64encode(img_bytes).decode('utf-8') +@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 chart_component = st.components.v2.component( @@ -377,13 +411,7 @@ export default function ({ ```python import streamlit as st - -with open("my_component/my_html.html", "r") as f: - HTML = f.read() -with open("my_component/my_css.css", "r") as f: - CSS = f.read() -with open("my_component/my_js.js", "r") as f: - JS = f.read() +from my_component import HTML, CSS, JS # Interactive counter with both state and triggers counter = st.components.v2.component( @@ -696,13 +724,7 @@ export default function ({ ```python import streamlit as st - -with open("my_component/my_html.html", "r") as f: - HTML = f.read() -with open("my_component/my_css.css", "r") as f: - CSS = f.read() -with open("my_component/my_js.js", "r") as f: - JS = f.read() +from my_component import HTML, CSS, JS form_component = st.components.v2.component( "contact_form", From 92e0fcc80efcf797c67029e04f87e75a60ae5e0f Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 6 Dec 2025 12:16:37 -0800 Subject: [PATCH 08/26] Correct callback behavior --- .../custom-components/components-v2/create.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 2d314a3b6..513552d19 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -85,8 +85,8 @@ export default function (component) { - `key` (string): Unique identifier for this component instance. Use this to assist with tracking unique instances of your component in the DOM. - `data` (any): All data passed from Python via the `data` parameter. Use this to customize a component instance. - `parentElement` (HTMLElement): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements. -- `setStateValue` (function): JS function to communicate stateful values to your Python backend. -- `setTriggerValue` (function): JS function to communicate event-based trigger values to your Python backend. +- `setStateValue` (function): JS 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): JS 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. @@ -169,6 +169,17 @@ counter_component = st.components.v2.component( ) ``` +### Sending state and trigger values to Python + +You can send state and trigger values to Python by calling `setStateValue()` or `setTriggerValue()` in your JavaScript code. The first argument is the state or trigger key name, and the second argument is the value to set. + +```javascript +setStateValue("count", count); +setTriggerValue("clicked", true); +``` + +Both `setStateValue()` and `setTriggerValue()` will trigger a rerun of the script. For each call in JavaScript, the associated callback function in Python will be executed as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed in the order they were called before the script run. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. + ## Step 2: Component mounting After registration, you mount your component in your Streamlit app. This creates a specific instance of the component and is equivalent to calling native Streamlit commands like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output: @@ -275,7 +286,7 @@ For more information about theming and styling, see the [Theming & styling](/dev #### Event callbacks (`on__change` or `on__change`) -For each state and trigger value for your component, you must pass a callback function. This callback function ensures that all state and trigger values are consistently available in the component's result object. Use the name of the state or trigger value in a keyword argument named `on__change`. These callback function can be empty (`lambda: None`) or contain your own response logic. Whenever your JavaScript code calls `setStateValue()` or `setTriggerValue()`, your app will immediately rerun, executing the associated callback as a prefix. Therefore, you can't call both `setStateValue()` and `setTriggerValue()` in response to the same event. +For each state and trigger value for your component, you must pass a callback function. This callback function ensures that all state and trigger key-value pairs are consistently available in the component's result object. To create the callback function's keyword argument name, add an `on_` prefix and `_change` suffix to trigger or state key name (`on__change`). These callback functions can be empty (`lambda: None`) or contain your own response logic. Whenever your JavaScript code calls `setStateValue()` or `setTriggerValue()`, your app will immediately rerun, executing the associated callback as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed before the script run, in the order they were called. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. Continuing the [Interactive component](#interactive-component) example from the previous section, we add a callback function for the `count` state value. From 38795530e75aee4edbd522ec80c90d49114e2436 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 6 Dec 2025 12:40:21 -0800 Subject: [PATCH 09/26] Update create.md --- .../custom-components/components-v2/create.md | 34 ++++++++++++------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 513552d19..287a51c3a 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -7,7 +7,7 @@ keywords: custom components v2, create components, inline components, component # Create custom v2 components -Components v2 provides a modern, flexible approach to extending Streamlit with custom functionality. This guide will walk you through creating your first component using the inline development approach. For package-based components, see the [Package-based Components](/develop/concepts/custom-components/components-v2/package-based) guide. +Components v2 provides a modern, flexible approach to extending Streamlit with custom functionality. This guide will introduce the necessary concepts for creating custom components using the inline development approach. For package-based components, see the [Package-based Components](/develop/concepts/custom-components/components-v2/package-based) guide. ## Two-step component process @@ -42,10 +42,10 @@ my_component = st.components.v2.component( ### Registration parameters -- The `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. -- The `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. -- The `css` (optional) is the CSS styling for your component. -- The `js` (optional) is the JavaScript logic for your component. +- `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. +- `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. +- `css` (optional) is the CSS styling for your component. +- `js` (optional) is the JavaScript logic for your component. For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. File references are only supported for package-based components. When you create a package-based component, Streamlit serves the contents of the package's asset directory, making those resources available to your app's frontend. When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend, relative to the served asset directory. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). @@ -90,7 +90,10 @@ export default function (component) { -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`, either create a placeholder element within `html` to update or inject new elements into `parentElement`. +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`. @@ -115,7 +118,7 @@ For larger components, you can organize your code into separate files. However, ``` my_app/ ├── streamlit_app.py # Entrypoint file -└── frontend/ +└── component/ ├── component.css # Component styles ├── component.html # Component HTML └── component.js # Component JavaScript @@ -123,12 +126,17 @@ my_app/ ```python # Load HTML, CSS, and JS from external files -with open("frontend/component.css", "r") as f: - CSS = f.read() -with open("frontend/component.html", "r") as f: - HTML = f.read() -with open("frontend/component.js", "r") as f: - JS = f.read() +@st.cache_data +def load_component_code(): + with open("component/component.css", "r") as f: + CSS = f.read() + with open("component/component.html", "r") as f: + HTML = f.read() + with open("component/component.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", From ea09fb6fc85eeb917838ef739753a8d6b0750306 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 6 Dec 2025 14:36:15 -0800 Subject: [PATCH 10/26] Quotes --- .../components-v2/quickstart.md | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 152a8ad8f..32872f6f5 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -43,7 +43,7 @@ def handle_button_click(): my_component = st.components.v2.component( "interactive_button", - html="", + html="""""", css=""" button { border: none; @@ -57,8 +57,8 @@ my_component = st.components.v2.component( export default function(component) { const { setTriggerValue, parentElement } = component; - parentElement.querySelector('button').onclick = () => { - setTriggerValue('action', 'button_clicked'); + parentElement.querySelector("button").onclick = () => { + setTriggerValue("action", "button_clicked"); }; } """, @@ -130,7 +130,7 @@ button { ```markup - + ``` @@ -190,7 +190,7 @@ This example shows how to pass different data types to your component. It shows `my_component/my_html.html`: ```markup -
Loading data...
+
Loading data...
``` `my_component/my_js.js`: @@ -232,7 +232,7 @@ df = create_sample_df() 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') + return base64.b64encode(img_bytes).decode("utf-8") img_base64 = load_image_as_base64("favi.png") # Serialization is automatically handled by Streamlit components @@ -271,16 +271,16 @@ df = create_sample_df() 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') + return base64.b64encode(img_bytes).decode("utf-8") img_base64 = load_image_as_base64("favi.png") # Serialization is automatically handled by Streamlit components chart_component = st.components.v2.component( "data_display", - html="
Loading data...
", + html="""
Loading data...
""", js=""" export default function({ data, parentElement }) { - const container = parentElement.querySelector('#data-container'); + const container = parentElement.querySelector("#data-container"); const df = data.df; const userInfo = data.user_info; @@ -484,14 +484,14 @@ counter = st.components.v2.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 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 + setStateValue("count", count); // Persistent state }; incrementBtn.onclick = () => { @@ -507,7 +507,7 @@ counter = st.components.v2.component( resetBtn.onclick = () => { count = 0; updateDisplay(); - setTriggerValue('reset', true); // One-time trigger + setTriggerValue("reset", true); // One-time trigger }; // Initialize @@ -515,9 +515,9 @@ counter = st.components.v2.component( // Cleanup function return () => { - incrementBtn.removeEventListener('click', incrementBtn.onclick); - decrementBtn.removeEventListener('click', decrementBtn.onclick); - resetBtn.removeEventListener('click', resetBtn.onclick); + incrementBtn.removeEventListener("click", incrementBtn.onclick); + decrementBtn.removeEventListener("click", decrementBtn.onclick); + resetBtn.removeEventListener("click", resetBtn.onclick); }; } """ @@ -839,46 +839,46 @@ form_component = st.components.v2.component( """, js=""" export default function({ parentElement, setStateValue, setTriggerValue, data }) { - const form = parentElement.querySelector('#contact-form'); - const h3 = parentElement.querySelector('h3'); - const nameInput = parentElement.querySelector('#name'); - const emailInput = parentElement.querySelector('#email'); - const messageInput = parentElement.querySelector('#message'); - const saveDraftBtn = parentElement.querySelector('#save-draft'); - const status = parentElement.querySelector('#status'); + const form = parentElement.querySelector("#contact-form"); + const h3 = parentElement.querySelector("h3"); + const nameInput = parentElement.querySelector("#name"); + const emailInput = parentElement.querySelector("#email"); + const messageInput = parentElement.querySelector("#message"); + const saveDraftBtn = parentElement.querySelector("#save-draft"); + const status = parentElement.querySelector("#status"); // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights requestAnimationFrame(() => { - const container = parentElement.querySelector('.form-container'); - const headingSizes = getComputedStyle(form).getPropertyValue('--st-heading-font-sizes').trim(); - const headingWeights = getComputedStyle(form).getPropertyValue('--st-heading-font-weights').trim(); - const sizes = headingSizes.split(',').map(s => s.trim()); - const weights = headingWeights.split(',').map(s => s.trim()); + const container = parentElement.querySelector(".form-container"); + const headingSizes = getComputedStyle(form).getPropertyValue("--st-heading-font-sizes").trim(); + const headingWeights = getComputedStyle(form).getPropertyValue("--st-heading-font-weights").trim(); + const sizes = headingSizes.split(",").map(s => s.trim()); + const weights = headingWeights.split(",").map(s => s.trim()); if (sizes[2] && container) { - container.style.setProperty('--st-heading-font-size-h3', sizes[2]); + container.style.setProperty("--st-heading-font-size-h3", sizes[2]); } if (weights[2] && container) { - container.style.setProperty('--st-heading-font-weight-h3', weights[2]); + container.style.setProperty("--st-heading-font-weight-h3", weights[2]); } }); // Load draft if available const draft = data?.draft || {}; - nameInput.value = draft.name || ''; - emailInput.value = draft.email || ''; - messageInput.value = draft.message || ''; + nameInput.value = draft.name || ""; + emailInput.value = draft.email || ""; + messageInput.value = draft.message || ""; // Save draft const saveDraft = () => { - setStateValue('draft', { + setStateValue("draft", { name: nameInput.value, email: emailInput.value, message: messageInput.value }); - setTriggerValue('action', 'save_draft'); - status.textContent = 'Draft saved!'; - status.style.color = 'var(--st-green-color)'; - setTimeout(() => status.textContent = '', 2000); + setTriggerValue("action", "save_draft"); + status.textContent = "Draft saved!"; + status.style.color = "var(--st-green-color)"; + setTimeout(() => status.textContent = "", 2000); }; // Submit form @@ -886,15 +886,15 @@ form_component = st.components.v2.component( e.preventDefault(); if (!nameInput.value || !emailInput.value || !messageInput.value) { - status.textContent = 'Please fill all fields'; - status.style.color = 'var(--st-red-color)'; + status.textContent = "Please fill all fields"; + status.style.color = "var(--st-red-color)"; return; } - status.textContent = 'Message sent!'; - status.style.color = 'var(--st-blue-color)'; - setTimeout(() => status.textContent = '', 2000); - setTriggerValue('submit', { + status.textContent = "Message sent!"; + status.style.color = "var(--st-blue-color)"; + setTimeout(() => status.textContent = "", 2000); + setTriggerValue("submit", { name: nameInput.value, email: emailInput.value, message: messageInput.value @@ -902,12 +902,12 @@ form_component = st.components.v2.component( }; // Event listeners - only update on button clicks - saveDraftBtn.addEventListener('click', saveDraft); - form.addEventListener('submit', submitForm); + saveDraftBtn.addEventListener("click", saveDraft); + form.addEventListener("submit", submitForm); return () => { - saveDraftBtn.removeEventListener('click', saveDraft); - form.removeEventListener('submit', submitForm); + saveDraftBtn.removeEventListener("click", saveDraft); + form.removeEventListener("submit", submitForm); }; } """ From 0d8889b633b8be75f6cc92d207c684dcf3b91100 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 6 Dec 2025 19:40:25 -0800 Subject: [PATCH 11/26] Default parameter and other edits --- .../custom-components/components-v2/create.md | 124 ++++++++++++------ 1 file changed, 81 insertions(+), 43 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 287a51c3a..40a9d4229 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -21,19 +21,17 @@ Creating and using a custom component involves two distinct steps: Registration is where you define what your component looks like and how it behaves. Use [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component) to register a component: ```python -import streamlit as st - # Register a component my_component = st.components.v2.component( name="my_button", html="", - css="button { padding: 10px; background: blue; color: white; }", + 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); + parentElement.querySelector("button").onclick = () => { + setTriggerValue("clicked", true); }; } """ @@ -43,11 +41,13 @@ my_component = st.components.v2.component( ### Registration parameters - `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. -- `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. -- `css` (optional) is the CSS styling for your component. -- `js` (optional) is the JavaScript logic for your component. +- `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. In the previous example, this is a single HTML button element. +- `css` (optional) is the CSS styling for your component. In the previous example, the CSS sets the button's background color to the primary color from the Streamlit theme and sets the text color to white. +- `js` (optional) is the JavaScript logic for your component. In the previous example, the JavaScript listens for a click event on the button and sets the `clicked` trigger value to `true`. + +For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. File references are only supported for package-based components. -For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. File references are only supported for package-based components. When you create a package-based component, Streamlit serves the contents of the package's asset directory, making those resources available to your app's frontend. When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend, relative to the served asset directory. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). +When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend. For a package-based component, Streamlit serves the contents of the package's asset directory, which makes those resources available to your app's frontend and accessible through relative paths. Streamlit doesn't serve an asset directory for inline components. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). @@ -82,7 +82,7 @@ export default function (component) { ``` - `name` (string): Component name from your Python registration. -- `key` (string): Unique identifier for this component instance. Use this to assist with tracking unique instances of your component in the DOM. +- `key` (string): Unique identifier for this 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. - `parentElement` (HTMLElement): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements. - `setStateValue` (function): JS 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. @@ -101,9 +101,11 @@ Don't directly overwrite or replace `parentElement.innerHTML`. If you do, you wi #### Simple HTML component -In the following examples, we'll register a simple component that displays "Hello, World!" in a heading. We use the primary color from the Streamlit theme for the heading color. For more information about making your components theme-aware, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. +In the following examples, we'll register a simple component that displays "Hello, World!" in a heading. We use the primary color from the Streamlit theme for the heading color. For more information about making your components theme-aware, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. This example is completed at the end of this guide in the [Complete examples](#simple-html-component-complete-example) section. ```python +import streamlit as st + hello_component = st.components.v2.component( name="hello_world", html="

Hello, World!

", @@ -113,7 +115,7 @@ hello_component = st.components.v2.component( #### Using files for inline components -For larger components, you can organize your code into separate files. However, for inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Similar to the examples shown in the [Quickstart](/develop/concepts/custom-components/components-v2/quickstart) guide, you can read the files into strings and pass them to your inline component. For package-based components, you can pass file references instead. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). +For larger components, you can organize your code into separate files. However, for inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Similar to the [Quickstart examples](/develop/concepts/custom-components/components-v2/quickstart), you can read the files and pass their contents to your inline component. For package-based components, you can pass file references instead. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). ``` my_app/ @@ -148,7 +150,7 @@ file_component = st.components.v2.component( #### Interactive component -In the following example, we'll register a component that displays a counter and a button to increment the counter. The counter value is stored in the component's state and is updated when the button is clicked. The component also triggers an event when the button is clicked. The component properties are destructured within the function signature directly. +In the following example, we'll register a component that displays a counter and a button to increment the counter. The counter value is stored in the component's state and is updated when the button is clicked. The component also triggers an event when the button is clicked. The component properties are destructured within the function signature directly. This example is completed at the end of this guide in the [Complete examples](#interactive-counter-complete-example) section. ```python import streamlit as st @@ -156,7 +158,7 @@ import streamlit as st counter_component = st.components.v2.component( name="counter", html=""" -
+
0
@@ -179,20 +181,27 @@ counter_component = st.components.v2.component( ### Sending state and trigger values to Python -You can send state and trigger values to Python by calling `setStateValue()` or `setTriggerValue()` in your JavaScript code. The first argument is the state or trigger key name, and the second argument is the value to set. +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 key name, and the second argument is the value to set. ```javascript setStateValue("count", count); setTriggerValue("clicked", true); ``` -Both `setStateValue()` and `setTriggerValue()` will trigger a rerun of the script. For each call in JavaScript, the associated callback function in Python will be executed as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed in the order they were called before the script run. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. +Both `setStateValue()` and `setTriggerValue()` will trigger a rerun of the script. For each call in JavaScript, the associated callback function in Python will be executed as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed in the order they were called before the script run. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. This is described later, in [Event callbacks](#event-callbacks-on_trigger_change-or-on_state_change) section. ## Step 2: Component mounting -After registration, you mount your component in your Streamlit app. This creates a specific instance of the component and is equivalent to calling native Streamlit commands like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output: +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 native Streamlit commands like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output. ```python +import streamlit as st + +# Register the component +my_component = st.components.v2.component( + # ... component registration code ... +) + # Mount the component result = my_component( key="unique_instance", @@ -211,8 +220,8 @@ Components use the Python `key` parameter in the same manner as widgets. For a d 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 iterate on the existing component instance when other parameters change. -- If you don't pass a key when you mount your component, Streamlit will create a new component instance when other parameters change. This will reset stateful values. +- If you pass a key when you mount your component, Streamlit will update the existing frontend elements when other parameters change. +- If you don't pass a key when you mount your component, Streamlit will create new frontend elements when other parameters change. This will reset the component's state values. Additionally, you must use keys to disambiguate between otherwise identical instances of the same component. @@ -230,34 +239,19 @@ The `key` property available in the `ComponentArgs` type isn't the same as the P #### Customizing and updating an instance (`data` and `default`) -A component instance can be customized and updated through two parameters in its mounting command that pass data between Python and JavaScript. +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 suppores JSON-serizable, Arrow-seriable, and raw bytes data. Commonly this is a single value or a dictionary of values that you retrieve in your JavaScript function. +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. -The `default` parameter sets initial values for component state. This is a dictionary where each key is a state attribute with an accompanying callback function passed as a keyword argument named `on__change`. +This code snippet is from the [Rich data exchange](/develop/concepts/custom-components/components-v2/quickstart#rich-data-exchange) quickstart example. It demonstrates passing a dictionary of values to the component's `data` parameter. ```python -import pandas as pd - -# Create sample data -df = pd.DataFrame({"A": [1, 2, 3], "B": [4, 5, 6]}) - -result = my_component( +result = chart_component( data={ - "user_name": "Alice", - "settings": {"theme": "dark", "notifications": True}, - "dataframe": df, # Auto-converted to Arrow format - "raw_bytes": b"binary data", - "nested_data": { - "level1": { - "level2": ["item1", "item2", "item3"] - } - }, - "number_list": [1, 2, 3, 4, 5] - }, - default={"count": 0, "selected_item": None}, - on_count_change=handle_count_change, - on_selected_item_change=handle_selection + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64 # Image as base64 string + } ) ``` @@ -267,6 +261,46 @@ DataFrames are automatically serialized using Apache Arrow format, which provide +The `default` parameter sets the initial values for component state _in Python_. This is a dictionary where each key is a state key name. Each state key 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 initial state. + +In general, the `default` parameter is used to avoid a rerun of the script when the component is mounted. If your component immediately calls `setStateValue()` when it's mounted, this can increase the chance of visual flickering. Hense, use the `default` parameter to avoid an unnecessary rerun. + +The following example demonstrates how to use the `default` parameter to avoid a rerun of the script when the component is mounted. The simple checkbox is given an initial state of `True`. + +```python +import streamlit as st + +simple_component = st.components.v2.component( + name="counter", + 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 +) +result +``` + +In the previous example, if the default wasn't set, then the initial state of the `"enabled"` key would be `None`. Thus, the return value would not be consistent with the component's state until the first user interaction triggers the event listener. + #### 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 throughout 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. @@ -337,7 +371,11 @@ st.write(f"Current count: {st.session_state.counter_1.count}") 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 Triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) guide. -## Interactive counter complete example +## Complete examples + +### Simple HTML component complete example + +### Interactive counter complete example Here's the complete example from the previous sections that demonstrates both registration and mounting. We've added some minimal CSS to make the component look more Streamlit-like and theme-compatible. For more information about theming and styling, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. From 310f3f7a867d99b5fcdcc769c528fb98203ba3eb Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 7 Dec 2025 13:00:45 -0800 Subject: [PATCH 12/26] Update create.md --- .../custom-components/components-v2/create.md | 255 +++++++++++++++--- 1 file changed, 220 insertions(+), 35 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 40a9d4229..62b88f9fd 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -120,21 +120,21 @@ For larger components, you can organize your code into separate files. However, ``` my_app/ ├── streamlit_app.py # Entrypoint file -└── component/ - ├── component.css # Component styles - ├── component.html # Component HTML - └── component.js # Component JavaScript +└── my_component/ + ├── my_css.css # Component styles + ├── my_html.html # Component HTML + └── my_js.js # Component JavaScript ``` ```python # Load HTML, CSS, and JS from external files @st.cache_data def load_component_code(): - with open("component/component.css", "r") as f: + with open("my_component/my_css.css", "r") as f: CSS = f.read() - with open("component/component.html", "r") as f: + with open("my_component/my_html.html", "r") as f: HTML = f.read() - with open("component/component.js", "r") as f: + with open("my_component/my_js.js", "r") as f: JS = f.read() return HTML, CSS, JS @@ -166,13 +166,13 @@ counter_component = st.components.v2.component( js=""" export default function({ parentElement, setStateValue }) { let count = 0; - const display = parentElement.querySelector('#count'); - const button = parentElement.querySelector('#increment'); + const display = parentElement.querySelector("#count"); + const button = parentElement.querySelector("#increment"); button.onclick = () => { count++; display.textContent = count; - setStateValue('count', count); + setStateValue("count", count); }; } """ @@ -261,9 +261,9 @@ DataFrames are automatically serialized using Apache Arrow format, which provide -The `default` parameter sets the initial values for component state _in Python_. This is a dictionary where each key is a state key name. Each state key 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 initial state. +The `default` parameter sets the initial values for component 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. -In general, the `default` parameter is used to avoid a rerun of the script when the component is mounted. If your component immediately calls `setStateValue()` when it's mounted, this can increase the chance of visual flickering. Hense, use the `default` parameter to avoid an unnecessary rerun. +In general, the `default` parameter is used to avoid a rerun of the script when the component is mounted. If your component immediately calls `setStateValue()` when it's mounted, this can increase the chance of visual flickering. Hence, use the `default` parameter to avoid an unnecessary rerun. The following example demonstrates how to use the `default` parameter to avoid a rerun of the script when the component is mounted. The simple checkbox is given an initial state of `True`. @@ -299,16 +299,49 @@ result = simple_component( result ``` -In the previous example, if the default wasn't set, then the initial state of the `"enabled"` key would be `None`. Thus, the return value would not be consistent with the component's state until the first user interaction triggers the event listener. +In the previous example, if the default wasn't set, then the initial state of the `"enabled"` key would be `None`. Thus, the return value would not be consistent with the component's state until the first user interaction triggers the event listener. It's possible to create the same component without using the `default` parameter, but this would make the app rerun unnecessarily when the component is mounted: + +```diff +import streamlit as st + +simple_component = st.components.v2.component( + name="counter", + 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; ++ setStateValue("enabled", enabled); + + // Update state when checkbox is toggled + checkbox.addEventListener("change", () => { + setStateValue("enabled", checkbox.checked); + }); + } + """ +) + +initial_state = False + +result = simple_component( + data={"enabled": initial_state}, +- default={"enabled": initial_state}, + on_enabled_change=lambda: None +) +result +``` #### 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 throughout 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. +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 + height=400 # Fixed height ) ``` @@ -328,18 +361,23 @@ For more information about theming and styling, see the [Theming & styling](/dev #### Event callbacks (`on__change` or `on__change`) -For each state and trigger value for your component, you must pass a callback function. This callback function ensures that all state and trigger key-value pairs are consistently available in the component's result object. To create the callback function's keyword argument name, add an `on_` prefix and `_change` suffix to trigger or state key name (`on__change`). These callback functions can be empty (`lambda: None`) or contain your own response logic. Whenever your JavaScript code calls `setStateValue()` or `setTriggerValue()`, your app will immediately rerun, executing the associated callback as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed before the script run, in the order they were called. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. +For each state and trigger value for your component, you must pass a callback function to the component mounting command. This callback function ensures that its state or trigger value is consistently available in the component's result object. -Continuing the [Interactive component](#interactive-component) example from the previous section, we add a callback function for the `count` state value. +If you have a trigger named `"click"`, then you have to pass a callback function to the keyword argument `on_click_change`. In general, to create the callback function keyword argument name, prefix your state or trigger name with `on_` and then suffix it with `_change`. These callback functions can be empty (`lambda: None`) or contain your own response logic. + +Whenever your JavaScript code calls `setStateValue()` or `setTriggerValue()`, your app will immediately rerun, executing the associated callback as a prefix to the script run. If you make multiple calls to `setStateValue()` or `setTriggerValue()` within the same event handler, their callbacks will be executed before the script run, in the order they were called. However, for `setStateValue()`, the callback function will only be executed if the state value changed as a result of the call. + +Continuing the [Interactive component](#interactive-component) example from the previous section, we add a callback function for the `count` state. ```python # Define callback function for the count state value def handle_count_change(): - # Called when the component calls setStateValue('count', value) + # Called when the component calls setStateValue("count", value) st.toast("Count was updated!") # Mount the counter component with callback result = counter_component( + width="content", on_count_change=handle_count_change, key="counter_1" ) @@ -351,7 +389,7 @@ You can access the state and trigger values of a component through the mounting ### Component return value -Components return a [`BidiComponentResult`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentresult) object that provides access to component state and trigger values. From the previous example, you can access the `count` state value as `result.count`. +Components return a [`BidiComponentResult`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentresult) object that provides access to component state and trigger values. In the previous ("Interactive component") example, you can access the `count` state as `result.count`. ```python # Access the current count value @@ -360,7 +398,7 @@ st.write(f"Current count: {result.count}") ### Component values in Session State -If you mounted your component with a key, you can access the component values through Session State. In the previous example, you can equivalently access the `count` state value as `st.session_state.counter_1.count`. +If you mounted your component with a key, you can access the component values through Session State. In the previous ("Interactive component") example, you can equivalently access the `count` state as `st.session_state.counter_1.count`. ```python # Access the current count value @@ -369,15 +407,162 @@ st.write(f"Current count: {st.session_state.counter_1.count}") ### 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 Triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) guide. +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. ## Complete examples ### Simple HTML component complete example +The `hello_component` introduced in a previous section can be completed by just mounting it. + +`streamlit_app.py`: + +```python +import streamlit as st + +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }" +) + +hello_component() +``` + +To fully make the component theme-compatible, you can add more CSS to use the theme's heading font and weight. Because the theme variables for headings are only available as arrays, this requires some JavaScript logic as well. For more information, see [Theming & styling](/develop/concepts/custom-components/components-v2/theming). + ### Interactive counter complete example -Here's the complete example from the previous sections that demonstrates both registration and mounting. We've added some minimal CSS to make the component look more Streamlit-like and theme-compatible. For more information about theming and styling, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. +For better syntax highlighting, the example is broken down into separate files like the [Quickstart examples](/develop/concepts/custom-components/components-v2/quickstart). The raw code is imported from an `__init__.py` file. The complete code in a single file is provided at the end of each example for easier copying and pasting. + +``` +project_directory/ +├── my_component/ +│ ├── __init__.py +│ ├── my_css.css +│ ├── my_html.html +│ └── my_js.js +└── streamlit_app.py +``` + + + +```python +import streamlit as st +from pathlib import Path + +# Get the current file's directory +_COMPONENT_DIR = Path(__file__).parent + +@st.cache_data +def load_html(): + with open(_COMPONENT_DIR / "my_html.html", "r") as f: + return f.read() + +@st.cache_data +def load_css(): + with open(_COMPONENT_DIR / "my_css.css", "r") as f: + return f.read() + +@st.cache_data +def load_js(): + with open(_COMPONENT_DIR / "my_js.js", "r") as f: + return f.read() + +HTML = load_html() +CSS = load_css() +JS = load_js() +``` + + + +The following example includes some minimal CSS to make the component look more Streamlit-like and theme-compatible. For more information about theming and styling, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. + +`my_component/my_html.html`: + +```markup +
+ 0 + +
+``` + +`my_component/my_css.css`: + +```css +.counter { + padding: 0.5rem 0.5rem; + border: 1px solid var(--st-border-color); + border-radius: var(--st-base-radius); + font-family: var(--st-font); + font-size: var(--st-base-font-size); + color: var(--st-text-color); +} +#count { + padding: 0.75rem; +} +#increment { + background: var(--st-primary-color); + color: white; + border: none; + border-radius: var(--st-button-radius); + padding: 0.25rem 0.5rem; + margin-left: 0.25rem; +} +#increment:hover { + opacity: 0.8; +} +``` + +`my_component/my_js.js`: + +```javascript +export default function ({ parentElement, setStateValue }) { + let count = 0; + const display = parentElement.querySelector("#count"); + const button = parentElement.querySelector("#increment"); + + button.onclick = () => { + count++; + display.textContent = count; + setStateValue("count", count); + }; +} +``` + +`streamlit_app.py`: + +```python +import streamlit as st +from my_component import HTML, CSS, JS + +counter_component = st.components.v2.component( + name="counter", + html=HTML, + css=CSS, + js=JS +) + +# Define callback function for the count state value +def handle_count_change(): + # Called when the component calls setStateValue("count", value) + st.toast("Count was updated!") + +# Mount the counter component with callback +result = counter_component( + width="content", + on_count_change=handle_count_change, + key="counter_1" +) + +# Access the current count value +st.write(f"Current count: {result.count}") + +# Access the current count value in Session State +st.write(f"Current count: {st.session_state.counter_1.count}") +``` + + ```python import streamlit as st @@ -392,26 +577,23 @@ counter_component = st.components.v2.component( """, css=""" .counter { - display: flex; - align-items: center; - justify-content: space-between; - padding: 8px 12px; + padding: .5rem .5rem; border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); font-family: var(--st-font); - } - #count { font-size: var(--st-base-font-size); color: var(--st-text-color); } + #count { + padding: .75rem; + } #increment { background: var(--st-primary-color); color: white; border: none; border-radius: var(--st-button-radius); - padding: 4px 8px; - font-family: var(--st-font); - font-size: var(--st-base-font-size); + padding: .25rem .5rem; + margin-left: .25rem; } #increment:hover { opacity: 0.8; @@ -420,13 +602,13 @@ counter_component = st.components.v2.component( js=""" export default function({ parentElement, setStateValue }) { let count = 0; - const display = parentElement.querySelector('#count'); - const button = parentElement.querySelector('#increment'); + const display = parentElement.querySelector("#count"); + const button = parentElement.querySelector("#increment"); button.onclick = () => { count++; display.textContent = count; - setStateValue('count', count); + setStateValue("count", count); }; } """ @@ -434,11 +616,12 @@ counter_component = st.components.v2.component( # Define callback function for the count state value def handle_count_change(): - # Called when the component calls setStateValue('count', value) + # Called when the component calls setStateValue("count", value) st.toast("Count was updated!") # Mount the counter component with callback result = counter_component( + width="content", on_count_change=handle_count_change, key="counter_1" ) @@ -450,6 +633,8 @@ st.write(f"Current count: {result.count}") st.write(f"Current count: {st.session_state.counter_1.count}") ``` + + ## What's next? Now that you understand the basics of component registration and mounting: From 90d0bb09a8b1fba4a6e4535284f01123b1b4df6e Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 7 Dec 2025 13:25:46 -0800 Subject: [PATCH 13/26] Format markdown table --- .../components-v2/state-and-triggers.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) 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 index 4590ad767..59183933e 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -27,7 +27,7 @@ State values have the following behavior: **Purpose**: Signal one-time events or user interactions. -**When to use**: For user actions like clicks, form submissions, or other discrete events +**When to use**: For user actions like clicks, form submissions, or other discrete events. Trigger values have the following behavior: @@ -38,13 +38,12 @@ Trigger values have the following behavior: ## 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)` | - -The callback pattern and value access are the same for both state and trigger values. +| 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 From add8eb07cda94447e6975de785b825ff65477d6c Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 7 Dec 2025 17:48:34 -0800 Subject: [PATCH 14/26] Debug form validation --- .../components-v2/quickstart.md | 97 +++++++++++++------ 1 file changed, 65 insertions(+), 32 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 32872f6f5..68fec8634 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -178,6 +178,12 @@ if result.action: The remaining examples on this page will use this file structure for easier viewing of the embedded code blocks. The complete code is provided at the end of each example for easier copying and pasting. + + +To avoid repeat warnings about re-registering the component, you can register your component in another module and import it. The standalone examples on this page are simple enough that this issue isn't apparent, but with more complex apps and components, this can be a nuisance. + + + ## Rich data exchange This example shows how to pass different data types to your component. It shows the following key concepts: @@ -644,7 +650,6 @@ export default function ({ data, }) { const form = parentElement.querySelector("#contact-form"); - const h3 = parentElement.querySelector("h3"); const nameInput = parentElement.querySelector("#name"); const emailInput = parentElement.querySelector("#email"); const messageInput = parentElement.querySelector("#message"); @@ -671,10 +676,13 @@ export default function ({ }); // Load draft if available - const draft = data?.draft || {}; - nameInput.value = draft.name || ""; - emailInput.value = draft.email || ""; - messageInput.value = draft.message || ""; + const loadDraft = (draft) => { + nameInput.value = draft.name || ""; + emailInput.value = draft.email || ""; + messageInput.value = draft.message || ""; + }; + + loadDraft(data?.draft || {}); // Save draft const saveDraft = () => { @@ -707,6 +715,8 @@ export default function ({ email: emailInput.value, message: messageInput.value, }); + loadDraft({}); + setStateValue("draft", {}); }; // Event listeners - only update on button clicks @@ -735,13 +745,17 @@ form_component = st.components.v2.component( # Handle form actions def handle_form_action(): - # Clear the draft on submit - st.session_state.message_form.draft={} + # Process submission + # if submission_failed: + # submission = st.session_state.message_form.submit + # st.session_state.message_form.draft=submission + pass # Use the component form_state = st.session_state.get("message_form", {}) result = form_component( data={"draft": form_state.get("draft", {})}, + default={"draft": form_state.get("draft", {})}, on_draft_change=lambda: None, on_submit_change=handle_form_action, key="message_form" @@ -779,7 +793,7 @@ form_component = st.components.v2.component( """, css=""" .form-container { - padding: 1rem;; + padding: 1rem; border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); box-sizing: border-box; @@ -789,10 +803,11 @@ form_component = st.components.v2.component( font-weight: var(--st-heading-font-weight-h3, inherit); margin: 0; } - input, textarea { + input, + textarea { width: 100%; - padding: .5rem; - margin: .5rem 0; + padding: 0.5rem; + margin: 0.5rem 0; background: var(--st-secondary-background-color); border: 1px solid transparent; border-radius: var(--st-base-radius); @@ -800,10 +815,11 @@ form_component = st.components.v2.component( font-size: inherit; font-family: inherit; } - input:focus, textarea:focus { + input:focus, + textarea:focus { outline: none; border-color: var(--st-primary-color); - }; + } textarea { height: 5rem; resize: vertical; @@ -811,10 +827,10 @@ form_component = st.components.v2.component( .form-actions { display: flex; gap: 1rem; - margin-top: .75rem; + margin-top: 0.75rem; } button { - padding: .5rem 1rem;; + padding: 0.5rem 1rem; border-radius: var(--st-button-radius); border: 1px solid transparent; font-size: inherit; @@ -834,13 +850,17 @@ form_component = st.components.v2.component( border-color: var(--st-primary-color); } #status { - margin-top: .5rem; + margin-top: 0.5rem; } """, js=""" - export default function({ parentElement, setStateValue, setTriggerValue, data }) { + export default function ({ + parentElement, + setStateValue, + setTriggerValue, + data, + }) { const form = parentElement.querySelector("#contact-form"); - const h3 = parentElement.querySelector("h3"); const nameInput = parentElement.querySelector("#name"); const emailInput = parentElement.querySelector("#email"); const messageInput = parentElement.querySelector("#message"); @@ -850,10 +870,14 @@ form_component = st.components.v2.component( // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights requestAnimationFrame(() => { const container = parentElement.querySelector(".form-container"); - const headingSizes = getComputedStyle(form).getPropertyValue("--st-heading-font-sizes").trim(); - const headingWeights = getComputedStyle(form).getPropertyValue("--st-heading-font-weights").trim(); - const sizes = headingSizes.split(",").map(s => s.trim()); - const weights = headingWeights.split(",").map(s => s.trim()); + const headingSizes = getComputedStyle(form) + .getPropertyValue("--st-heading-font-sizes") + .trim(); + const headingWeights = getComputedStyle(form) + .getPropertyValue("--st-heading-font-weights") + .trim(); + const sizes = headingSizes.split(",").map((s) => s.trim()); + const weights = headingWeights.split(",").map((s) => s.trim()); if (sizes[2] && container) { container.style.setProperty("--st-heading-font-size-h3", sizes[2]); } @@ -863,22 +887,25 @@ form_component = st.components.v2.component( }); // Load draft if available - const draft = data?.draft || {}; - nameInput.value = draft.name || ""; - emailInput.value = draft.email || ""; - messageInput.value = draft.message || ""; + const loadDraft = (draft) => { + nameInput.value = draft.name || ""; + emailInput.value = draft.email || ""; + messageInput.value = draft.message || ""; + }; + + loadDraft(data?.draft || {}); // Save draft const saveDraft = () => { setStateValue("draft", { name: nameInput.value, email: emailInput.value, - message: messageInput.value + message: messageInput.value, }); setTriggerValue("action", "save_draft"); status.textContent = "Draft saved!"; status.style.color = "var(--st-green-color)"; - setTimeout(() => status.textContent = "", 2000); + setTimeout(() => (status.textContent = ""), 2000); }; // Submit form @@ -893,12 +920,14 @@ form_component = st.components.v2.component( status.textContent = "Message sent!"; status.style.color = "var(--st-blue-color)"; - setTimeout(() => status.textContent = "", 2000); + setTimeout(() => (status.textContent = ""), 2000); setTriggerValue("submit", { name: nameInput.value, email: emailInput.value, - message: messageInput.value + message: messageInput.value, }); + loadDraft({}); + setStateValue("draft", {}); }; // Event listeners - only update on button clicks @@ -915,13 +944,17 @@ form_component = st.components.v2.component( # Handle form actions def handle_form_action(): - # Clear the draft on submit - st.session_state.message_form.draft={} + # Process submission + # if submission_failed: + # submission = st.session_state.message_form.submit + # st.session_state.message_form.draft=submission + pass # Use the component form_state = st.session_state.get("message_form", {}) result = form_component( data={"draft": form_state.get("draft", {})}, + default={"draft": form_state.get("draft", {})}, on_draft_change=lambda: None, on_submit_change=handle_form_action, key="message_form" From 1b767220df6060ae9ecc8aae305224f7803ad09b Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 7 Dec 2025 21:00:00 -0800 Subject: [PATCH 15/26] Radial menu state example --- .../components-v2/state-and-triggers.md | 380 +++++++++++++----- 1 file changed, 280 insertions(+), 100 deletions(-) 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 index 59183933e..bdd96f6bb 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -45,121 +45,301 @@ Trigger values have the following behavior: | 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 +## State values in practice: Radial menu component -State values are perfect for tracking the ongoing state of your component. Here's a practical example: +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 compoenent 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 -# Create a component that tracks user preferences -preferences_component = st.components.v2.component( - name="user_preferences", - html=""" -
-

User Preferences

- -
- -
- -
- """, - css=""" - .preferences { - padding: 20px; - border: 1px solid var(--st-border-color); - border-radius: 8px; - font-family: var(--st-font); - } - label { - display: block; - margin: 10px 0; - } - """, - js=""" - export default function({ parentElement, setStateValue, data }) { - const notifications = parentElement.querySelector('#notifications'); - const theme = parentElement.querySelector('#theme'); - const fontSize = parentElement.querySelector('#font-size'); - const fontDisplay = parentElement.querySelector('#font-display'); - - // Initialize with default values - const defaults = data?.defaults || {}; - notifications.checked = defaults.notifications || false; - theme.value = defaults.theme || 'light'; - fontSize.value = defaults.fontSize || 14; - fontDisplay.textContent = fontSize.value; - - // Update state when values change - const updateState = () => { - setStateValue('notifications', notifications.checked); - setStateValue('theme', theme.value); - setStateValue('fontSize', parseInt(fontSize.value)); - }; +component_dir = Path(__file__).parent - // Handle font size changes with display update - const handleFontSizeChange = () => { - fontDisplay.textContent = fontSize.value; - updateState(); - }; - // Attach event listeners - notifications.addEventListener('change', updateState); - theme.addEventListener('change', updateState); - fontSize.addEventListener('input', handleFontSizeChange); +@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 - // Initialize state - updateState(); - // Cleanup - return () => { - notifications.removeEventListener('change', updateState); - theme.removeEventListener('change', updateState); - fontSize.removeEventListener('input', handleFontSizeChange); - }; - } - """ +HTML, CSS, JS = load_component_code() + +radial_menu = st.components.v2.component( + name="radial_menu", + html=HTML, + css=CSS, + js=JS, ) +``` -# Use the component with state tracking -def handle_preferences_change(): - # This callback runs whenever any preference changes - st.rerun() # Optional: force immediate UI update - -result = preferences_component( - key="user_prefs", - data={"defaults": {"notifications": True, "theme": "dark", "fontSize": 16}}, - default={"notifications": False, "theme": "light", "fontSize": 14}, - on_notifications_change=handle_preferences_change, - on_theme_change=handle_preferences_change, - on_fontSize_change=handle_preferences_change +
+ +### 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", ) -# Access persistent state values -st.write("**Current Preferences:**") -st.write(f"- Notifications: {result.notifications}") -st.write(f"- Theme: {result.theme}") -st.write(f"- Font Size: {result.fontSize}px") - -# Use the preferences to configure your app -if result.theme == "dark": - st.markdown("🌙 Dark theme selected") -else: - st.markdown("☀️ Light theme selected") +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. Here's an example: From cee43b4146875714738076415dceb28ed99b986f Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sun, 7 Dec 2025 21:21:47 -0800 Subject: [PATCH 16/26] Radial menu one-page code --- .../components-v2/state-and-triggers.md | 323 ++++++++++++++++++ 1 file changed, 323 insertions(+) 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 index bdd96f6bb..c394cf016 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -340,6 +340,329 @@ if result.selection: + + +```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. Here's an example: From b9a4eec4ed7f2cb73cd04e09b60fa6c3e411cede Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Mon, 8 Dec 2025 00:52:57 -0800 Subject: [PATCH 17/26] Danger button --- .../components-v2/state-and-triggers.md | 379 +++++++++++++----- 1 file changed, 279 insertions(+), 100 deletions(-) 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 index c394cf016..c3980f5a8 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -665,141 +665,320 @@ if result.selection: ## Trigger values in practice -Trigger values are ideal for handling discrete user actions. Here's an example: +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 -# Initialize session state for tracking actions -if "action_log" not in st.session_state: - st.session_state.action_log = [] -if "save_count" not in st.session_state: - st.session_state.save_count = 0 +st.title("Hold-to-Confirm Button") +st.caption("A dangerous action that requires intentional confirmation") -# Create a component with various action buttons -action_component = st.components.v2.component( - name="action_buttons", +# 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=""" -
-

Document Actions

- - - - +
+
+ ⚠️ + Danger Zone +
+ + + +

Press and hold for 2 seconds to confirm

""", css=""" - .actions { - padding: 20px; - border: 1px solid var(--st-border-color); - border-radius: 8px; + .danger-zone { + font-family: var(--st-font); + padding: 2rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.5rem; } - button { - margin: 5px; - padding: 10px 15px; - border: none; - border-radius: 4px; - cursor: pointer; - font-size: 14px; + + .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); } - .primary { - background: var(--st-primary-color); - color: white; + + .warning-icon { + font-size: 1rem; + } + + .warning-text { + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--st-red-color); } - .secondary { + + .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); - border: 1px solid var(--st-border-color); + opacity: 0.6; + transition: all 0.3s ease; } - .danger { - background: var(--st-red-color); - color: white; + + .hold-button.holding .label { + color: var(--st-red-color); + opacity: 1; } - button:hover { - opacity: 0.8; + + .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 saveBtn = parentElement.querySelector('#save'); - const exportBtn = parentElement.querySelector('#export'); - const shareBtn = parentElement.querySelector('#share'); - const deleteBtn = parentElement.querySelector('#delete'); - - // Handle different actions with trigger values - const handleSave = () => { - setTriggerValue('action', 'save'); - }; + const button = parentElement.querySelector("#danger-btn"); + const progress = parentElement.querySelector("#ring-progress"); + const icon = parentElement.querySelector("#icon"); + const label = parentElement.querySelector("#label"); - const handleExport = () => { - setTriggerValue('action', 'export'); - }; + const HOLD_DURATION = 2000; // 2 seconds + const COOLDOWN_DURATION = 1500; // cooldown after trigger + const CIRCUMFERENCE = 2 * Math.PI * 45; // circle circumference - const handleShare = () => { - setTriggerValue('action', 'share'); - }; + 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; - const handleDelete = () => { - // Confirm before triggering delete - if (confirm('Are you sure you want to delete this document?')) { - setTriggerValue('action', 'delete'); + if (progressPercent >= 1) { + // Triggered! + triggerAction(); + } else { + animationFrame = requestAnimationFrame(updateProgress); } - }; + } - // Attach event listeners - saveBtn.addEventListener('click', handleSave); - exportBtn.addEventListener('click', handleExport); - shareBtn.addEventListener('click', handleShare); - deleteBtn.addEventListener('click', handleDelete); + function startHold() { + if (isDisabled) return; // Ignore if in cooldown - // Cleanup - return () => { - saveBtn.removeEventListener('click', handleSave); - exportBtn.removeEventListener('click', handleExport); - shareBtn.removeEventListener('click', handleShare); - deleteBtn.removeEventListener('click', handleDelete); - }; - } - """ -) + startTime = Date.now(); + button.classList.add("holding"); + label.textContent = "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } -# Define action handlers -def handle_action(): - action = result.action - timestamp = st.session_state.get('timestamp', 'Unknown time') + function cancelHold() { + if (isDisabled) return; // Ignore if in cooldown - if action == 'save': - st.session_state.save_count += 1 - st.session_state.action_log.append(f"Document saved (#{st.session_state.save_count})") - st.success("Document saved successfully!") + startTime = null; + button.classList.remove("holding"); + label.textContent = "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; - elif action == 'export': - st.session_state.action_log.append("Document exported") - st.info("Document exported to downloads folder") + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + } - elif action == 'share': - st.session_state.action_log.append("Share link generated") - st.info("Share link copied to clipboard!") + function triggerAction() { + cancelAnimationFrame(animationFrame); + animationFrame = null; + startTime = null; + isDisabled = true; // Disable during cooldown - elif action == 'delete': - st.session_state.action_log.append("Document deleted") - st.error("Document deleted permanently") + button.classList.remove("holding"); + button.classList.add("triggered"); + button.disabled = true; -# Use the component -result = action_component( - key="doc_actions", - on_action_change=handle_action + 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); + }; + } + """, ) -# Show action feedback only when triggered -if result.action: - st.write(f"**Last action:** {result.action}") -# Display action log -if st.session_state.action_log: - st.write("**Action History:**") - for i, log_entry in enumerate(reversed(st.session_state.action_log[-5:]), 1): - st.write(f"{i}. {log_entry}") +# 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 From b5ed159e73ae09957cec1fdc3b3266843fd6c607 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Mon, 8 Dec 2025 04:15:35 -0800 Subject: [PATCH 18/26] Stopwatch example for combined state/trigger value --- .../components-v2/state-and-triggers.md | 587 ++++++++++++++---- 1 file changed, 452 insertions(+), 135 deletions(-) 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 index c3980f5a8..9d758bc2e 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -983,182 +983,499 @@ if st.session_state.deleted_items: ## Combining state and triggers -Many components benefit from using both patterns together. Here's a comprehensive example: +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 -# Initialize session state -if "form_submissions" not in st.session_state: - st.session_state.form_submissions = 0 +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 = [] -# Create a form component that uses both state and triggers -form_component = st.components.v2.component( - name="interactive_form", +stopwatch = st.components.v2.component( + name="stopwatch", html=""" -
-

Contact Form

-
- - - -
- - +
+
+ + + + +
+ 00 + : + 00 + . + 00
- -
+
+ +
+ + + +
+ +
""", css=""" - .form-container { - padding: 20px; - border: 1px solid var(--st-border-color); - border-radius: 8px; - max-width: 500px; - } - input, textarea { - width: 100%; - padding: 10px; - margin: 5px 0; - border: 1px solid var(--st-border-color); - border-radius: 4px; + .stopwatch { font-family: var(--st-font); + color: var(--st-text-color); + display: flex; + flex-direction: column; + align-items: center; + padding: 2rem; + gap: 2rem; } - textarea { - height: 100px; - resize: vertical; + + /* Ring Display */ + .display-ring { + position: relative; + width: 14rem; + height: 14rem; } - .form-actions { + + .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: 10px; - margin-top: 10px; + gap: 1rem; + align-items: center; } - button { - padding: 10px 15px; + + .ctrl-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.25rem; + padding: 0.75rem 1.25rem; border: none; - border-radius: 4px; + border-radius: var(--st-button-radius); cursor: pointer; + transition: all 0.2s cubic-bezier(0.34, 1.56, 0.64, 1); + min-width: 5rem; } - #save-draft { - background: var(--st-secondary-background-color); - color: var(--st-text-color); - border: 1px solid var(--st-border-color); + + .ctrl-btn:disabled { + opacity: 0.4; + cursor: not-allowed; + } + + .ctrl-btn:hover:not(:disabled) { + transform: scale(1.05); } - #submit { + + .ctrl-btn.primary { background: var(--st-primary-color); color: white; } - #status { - margin-top: 10px; - font-size: 14px; + + .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, setStateValue, setTriggerValue, data }) { - const form = parentElement.querySelector('#contact-form'); - const nameInput = parentElement.querySelector('#name'); - const emailInput = parentElement.querySelector('#email'); - const messageInput = parentElement.querySelector('#message'); - const saveDraftBtn = parentElement.querySelector('#save-draft'); - const submitBtn = parentElement.querySelector('#submit'); - const status = parentElement.querySelector('#status'); - - // Load draft data if available - const draft = data?.draft || {}; - nameInput.value = draft.name || ''; - emailInput.value = draft.email || ''; - messageInput.value = draft.message || ''; - - // Update state as user types (for draft saving) - const updateDraft = () => { - setStateValue('draft', { - name: nameInput.value, - email: emailInput.value, - message: messageInput.value + 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]; }); - }; - // Save draft action (trigger) - const saveDraft = () => { - updateDraft(); - setTriggerValue('action', 'save_draft'); - status.textContent = '✅ Draft saved'; - setTimeout(() => status.textContent = '', 2000); - }; + const minDelta = Math.min(...deltas); + const maxDelta = Math.max(...deltas); - // Submit form action (trigger) - const submitForm = (e) => { - e.preventDefault(); + // 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); - // Validate form - if (!nameInput.value || !emailInput.value || !messageInput.value) { - status.textContent = '❌ Please fill all fields'; - return; + 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); } + } - updateDraft(); - setTriggerValue('action', 'submit'); - status.textContent = '📤 Sending message...'; - }; + function start() { + isRunning = true; + lastTimestamp = null; + animationFrame = requestAnimationFrame(tick); + updateButtons(); + setStateValue("running", true); + } - // Attach event listeners - nameInput.addEventListener('input', updateDraft); - emailInput.addEventListener('input', updateDraft); - messageInput.addEventListener('input', updateDraft); - saveDraftBtn.addEventListener('click', saveDraft); - form.addEventListener('submit', submitForm); + function pause() { + isRunning = false; + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + updateButtons(); + setStateValue("running", false); + setStateValue("elapsed", elapsedMs); + } - // Initialize state - updateDraft(); + 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); + } - // Cleanup return () => { - nameInput.removeEventListener('input', updateDraft); - emailInput.removeEventListener('input', updateDraft); - messageInput.removeEventListener('input', updateDraft); - saveDraftBtn.removeEventListener('click', saveDraft); - form.removeEventListener('submit', submitForm); + if (animationFrame) cancelAnimationFrame(animationFrame); }; } """ ) -# Define action handlers -def handle_form_action(): - action = result.action - - if action == 'save_draft': - st.info("💾 Draft saved automatically") - - elif action == 'submit': - st.session_state.form_submissions += 1 - st.success(f"📤 Message sent successfully! (Submission #{st.session_state.form_submissions})") - # Clear the draft after successful submission - result.draft = {"name": "", "email": "", "message": ""} - -# Use the component -result = form_component( - key="contact_form", - data={"draft": st.session_state.get("form_draft", {})}, - default={"draft": {"name": "", "email": "", "message": ""}}, - on_draft_change=lambda: None, # Register draft as state - on_action_change=handle_form_action # Handle form actions +# 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": []}, ) -# Store draft in session state (persistent across reruns) -if result.draft: - st.session_state.form_draft = result.draft - -# Show current draft status -if result.draft and any(result.draft.values()): - st.write("**Current Draft:**") - if result.draft.get('name'): - st.write(f"- Name: {result.draft['name']}") - if result.draft.get('email'): - st.write(f"- Email: {result.draft['email']}") - if result.draft.get('message'): - st.write(f"- Message: {len(result.draft['message'])} characters") +# 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 @@ -1179,7 +1496,7 @@ if result.draft and any(result.draft.values()): ### Callback registration -Both state and trigger values require callback registration using the `on__change` pattern. This example mounts a component with callbacks for the following keys: +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 From c192f231746a996c5db2d9c8095f0f0a129cf40e Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Mon, 8 Dec 2025 15:37:06 -0800 Subject: [PATCH 19/26] Update theming.md --- .../components-v2/theming.md | 105 ++++++++++-------- 1 file changed, 57 insertions(+), 48 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/theming.md b/content/develop/concepts/custom-components/components-v2/theming.md index 38e41b5e9..491cda397 100644 --- a/content/develop/concepts/custom-components/components-v2/theming.md +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -39,10 +39,35 @@ In general, for any theme configuration option, use the CSS custom property `--s 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` | @@ -57,19 +82,12 @@ For example, to reference the primary color (`theme.primaryColor`), use `--st-pr | `--st-base-font-weight` | `theme.baseFontWeight` | | `--st-code-font-weight` | `theme.codeFontWeight` | | `--st-code-font-size` | `theme.codeFontSize` | -| `--st-heading-font-sizes` | `theme.headingFontSizes` | -| `--st-heading-font-weights` | `theme.headingFontWeights` | +| `--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-chart-categorical-colors` | `theme.chartCategoricalColors` | -| `--st-chart-sequential-colors` | `theme.chartSequentialColors` | -| `--st-heading-color` | `theme.headingColor` | -| `--st-border-color-light` | `theme.borderColorLight` | -| `--st-code-text-color` | `theme.codeTextColor` | -| `--st-widget-border-color` | `theme.widgetBorderColor` | | `--st-red-color` | `theme.redColor` | | `--st-orange-color` | `theme.orangeColor` | | `--st-yellow-color` | `theme.yellowColor` | @@ -92,19 +110,11 @@ For example, to reference the primary color (`theme.primaryColor`), use `--st-pr | `--st-violet-text-color` | `theme.violetTextColor` | | `--st-gray-text-color` | `theme.grayTextColor` | - - - - -Array properties like `--st-heading-font-sizes`, `--st-chart-categorical-colors`, and `--st-chart-sequential-colors` are exposed as comma-separated strings. You can parse these in JavaScript if needed for dynamic styling. - - - ## Practical theming examples ### Basic themed component -Here's a simple component that uses Streamlit's theming: +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 @@ -125,17 +135,18 @@ themed_card = st.components.v2.component( background: var(--st-secondary-background-color); border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); - padding: 20px; - margin: 10px 0; + padding: 1.25rem; + margin: 0.625rem 0; font-family: var(--st-font); - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + 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 10px 0; + margin: 0 0 0.625rem 0; font-weight: 600; } @@ -150,10 +161,8 @@ themed_card = st.components.v2.component( color: white; border: none; border-radius: var(--st-button-radius); - padding: 8px 16px; + padding: 0.5rem 1rem; cursor: pointer; - font-family: var(--st-font); - font-size: var(--st-base-font-size); transition: opacity 0.2s; } @@ -178,7 +187,7 @@ if result.button_click: ### Status message component -A component that uses Streamlit's status colors with a dynamically set status type and message: +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 @@ -195,10 +204,10 @@ status_component = st.components.v2.component( .status { display: flex; align-items: center; - padding: 12px 16px; - margin: 8px 0; + padding: 0.75rem 1rem; + margin: 0.5rem 0; border-radius: var(--st-base-radius); - border-left: 4px solid; + border-left: 0.25rem solid; font-family: var(--st-font); } @@ -227,8 +236,8 @@ status_component = st.components.v2.component( } .icon { - margin-right: 10px; - font-size: 16px; + margin-right: 0.625rem; + font-size: 1rem; } .message { @@ -283,7 +292,7 @@ status_component( ### Data table component -A component that matches Streamlit's dataframe styling: +You can use CSS Custom Properties to style a data table to match Streamlit's dataframe styling. ```python import streamlit as st @@ -339,14 +348,14 @@ data_table = st.components.v2.component( background: var(--st-dataframe-header-background-color); color: var(--st-text-color); font-weight: 600; - padding: 12px 16px; + 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: 12px 16px; + 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); @@ -361,9 +370,9 @@ data_table = st.components.v2.component( } .status-badge { - padding: 4px 8px; + padding: 0.25rem 0.5rem; border-radius: calc(var(--st-base-radius) / 2); - font-size: 0.85em; + font-size: 0.75rem; font-weight: 500; } @@ -427,7 +436,7 @@ non_isolated_component = st.components.v2.component( ## 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 creates a responsive grid layout that adapts to the screen size: +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 @@ -446,8 +455,8 @@ responsive_component = st.components.v2.component( .responsive-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); - gap: 16px; - padding: 16px; + gap: 1rem; + padding: 1rem; font-family: var(--st-font); } @@ -455,7 +464,7 @@ responsive_component = st.components.v2.component( background: var(--st-secondary-background-color); border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); - padding: 20px; + padding: 1.25rem; text-align: center; color: var(--st-text-color); transition: transform 0.2s; @@ -463,19 +472,19 @@ responsive_component = st.components.v2.component( .grid-item:hover { transform: translateY(-2px); - box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + 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: 12px; - padding: 12px; + gap: 0.75rem; + padding: 0.75rem; } .grid-item { - padding: 16px; + padding: 1rem; } } """ @@ -506,16 +515,16 @@ Instead of hardcoding colors, always use Streamlit's theme variables: ### 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. +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 based on their semantic meaning: +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-color); + color: var(--st-red-text-color); background: var(--st-red-background-color); } From 1ec769eb3520f0fe54410cb282bb89cdf433d16c Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Tue, 16 Dec 2025 16:02:59 -0800 Subject: [PATCH 20/26] Typos and link corrections --- .../concepts/custom-components/components-v1/_index.md | 4 ++-- .../concepts/custom-components/components-v2/create.md | 8 ++++---- .../custom-components/components-v2/package-based.md | 4 ++-- .../custom-components/components-v2/quickstart.md | 2 +- .../custom-components/components-v2/state-and-triggers.md | 6 +++--- .../concepts/custom-components/components-v2/theming.md | 6 +++--- content/develop/concepts/custom-components/overview.md | 2 +- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v1/_index.md b/content/develop/concepts/custom-components/components-v1/_index.md index 83f5623d9..f9a64644b 100644 --- a/content/develop/concepts/custom-components/components-v1/_index.md +++ b/content/develop/concepts/custom-components/components-v1/_index.md @@ -45,9 +45,9 @@ Components are super easy to use: If you're interested in making your own component, check out the following resources: -- [Create a Component](/develop/concepts/custom-components/create) +- [Create a Component](/develop/concepts/custom-components/components-v1/create) - [Publish a Component](/develop/concepts/custom-components/publish) -- [Components API](/develop/concepts/custom-components/intro) +- [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 diff --git a/content/develop/concepts/custom-components/components-v2/create.md b/content/develop/concepts/custom-components/components-v2/create.md index 62b88f9fd..9658e3b63 100644 --- a/content/develop/concepts/custom-components/components-v2/create.md +++ b/content/develop/concepts/custom-components/components-v2/create.md @@ -206,7 +206,7 @@ my_component = st.components.v2.component( result = my_component( key="unique_instance", data={"initial_value": 42}, - on_clicked_change=lambda: None) + on_clicked_change=lambda: None ) ``` @@ -639,7 +639,7 @@ st.write(f"Current count: {st.session_state.counter_1.count}") Now that you understand the basics of component registration and mounting: -- Learn about [State vs triggers](/develop/concepts/custom-components/v2/state-and-triggers) for advanced component communication. -- Explore [Theming and styling](/develop/concepts/custom-components/v2/theming) to make your components look great. -- Discover [Package-based components](/develop/concepts/custom-components/v2/package-based) for complex projects. +- Learn about [State vs triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) for advanced component communication. +- Explore [Theming and styling](/develop/concepts/custom-components/components-v2/theming) to make your components look great. +- Discover [Package-based components](/develop/concepts/custom-components/components-v2/package-based) for complex projects. - Check out the [JavaScript API reference](/develop/api-reference/custom-components/) for complete frontend documentation. diff --git a/content/develop/concepts/custom-components/components-v2/package-based.md b/content/develop/concepts/custom-components/components-v2/package-based.md index 24d713e10..fc774bfaa 100644 --- a/content/develop/concepts/custom-components/components-v2/package-based.md +++ b/content/develop/concepts/custom-components/components-v2/package-based.md @@ -739,6 +739,6 @@ Provide comprehensive documentation: Now that you understand package-based components: -- Learn about [State vs triggers](/develop/concepts/custom-components/v2/state-and-triggers) for interactive functionality. -- Explore [Theming and styling](/develop/concepts/custom-components/v2/theming) for beautiful 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/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 68fec8634..f6684c08f 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -320,7 +320,7 @@ This comprehensive example demonstrates both state and trigger values. It shows - Using CSS custom properties to style the component. - Bidirectional communication between Python and JavaScript. - Multiple event handlers. -- Cleanup functions for proper resource management +- Cleanup functions for proper resource management. `my_component/my_html.html`: 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 index 9d758bc2e..ebd32b418 100644 --- a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md +++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md @@ -49,7 +49,7 @@ Trigger values have the following behavior: 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 compoenent 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 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. @@ -1542,6 +1542,6 @@ result = my_component( Now that you understand state and trigger values: -- Learn about [Theming and styling](/develop/concepts/custom-components/v2/theming) to make your components look great. -- Explore [Package-based components](/develop/concepts/custom-components/v2/package-based) for complex projects with TypeScript. +- 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 index 491cda397..02f5e6b8c 100644 --- a/content/develop/concepts/custom-components/components-v2/theming.md +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -541,6 +541,6 @@ Streamlit's theme colors are designed with accessibility in mind. Maintain prope Now that you understand theming and styling: -- Explore [Package-based components](/develop/concepts/custom-components/v2/package-based) for advanced development workflows. -- Learn about [State vs triggers](/develop/concepts/custom-components/v2/state-and-triggers) for interactive components. -- Check out the [Create components](/develop/concepts/custom-components/v2/create) guide for more examples. +- 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 [Create components](/develop/concepts/custom-components/components-v2/create) guide for more examples. diff --git a/content/develop/concepts/custom-components/overview.md b/content/develop/concepts/custom-components/overview.md index 6b912c483..4c519344c 100644 --- a/content/develop/concepts/custom-components/overview.md +++ b/content/develop/concepts/custom-components/overview.md @@ -37,7 +37,7 @@ Components v1 is the original framework that has been stable and widely used sin 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 optimatized for bidirectional communication. +- **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 From 8f48ba0e937019139acc9dfa598649236dbea396 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Wed, 17 Dec 2025 08:45:07 -0800 Subject: [PATCH 21/26] Update quickstart.md --- .../components-v2/quickstart.md | 1054 +++++++++++------ 1 file changed, 696 insertions(+), 358 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index f6684c08f..6b2778fe9 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -23,14 +23,35 @@ Creating and using a custom component involves two distinct steps: For more information, see [Create custom v2 components](/develop/concepts/custom-components/components-v2/create). -## Simple interactive button +## Hello world component -This example shows the basics of creating an interactive component with bidirectional communication. It shows the following key concepts: +You can use custom components v2 to create static HTML and CSS components. The following example display "Hello, World!" in an H2 heading, using the app's primary color for the heading text. This example shows the following key concepts: + +- Component registration with HTML and CSS. +- Mounting a component with its command created from registration. +- Styling the component with the app's theme. + +```python +import streamlit as st + +hello_component = st.components.v2.component( + name="hello_world", + html="

Hello, World!

", + css="h2 { color: var(--st-primary-color); }", +) + +hello_component() +``` + + + +## Simple button component + +Your v2 component can set user data to your app. This example shows a simple button that sends a trigger value to your app when clicked. Trigger values are one-time events that are not persisted across reruns. This example shows the following key concepts: - Component registration with HTML, CSS, and JavaScript. - Trigger values using `setTriggerValue()`. - Callback functions with the `on__change` naming pattern. -- Mounting a component with its command created from registration. ```python import streamlit as st @@ -56,7 +77,6 @@ my_component = st.components.v2.component( js=""" export default function(component) { const { setTriggerValue, parentElement } = component; - parentElement.querySelector("button").onclick = () => { setTriggerValue("action", "button_clicked"); }; @@ -70,15 +90,17 @@ if result.action: st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") ``` -For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you'll need to read them into strings. The previous example is equivalent to the following: + + +For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you must read them into strings. The previous example is equivalent to the following: ``` project_directory/ ├── my_component/ │ ├── __init__.py -│ ├── my_css.css -│ ├── my_html.html -│ └── my_js.js +│ ├── component.css +│ ├── component.html +│ └── component.js └── streamlit_app.py ``` @@ -93,17 +115,17 @@ _COMPONENT_DIR = Path(__file__).parent @st.cache_data def load_html(): - with open(_COMPONENT_DIR / "my_html.html", "r") as f: + with open(_COMPONENT_DIR / "component.html", "r") as f: return f.read() @st.cache_data def load_css(): - with open(_COMPONENT_DIR / "my_css.css", "r") as f: + with open(_COMPONENT_DIR / "component.css", "r") as f: return f.read() @st.cache_data def load_js(): - with open(_COMPONENT_DIR / "my_js.js", "r") as f: + with open(_COMPONENT_DIR / "component.js", "r") as f: return f.read() HTML = load_html() @@ -113,7 +135,15 @@ JS = load_js() - + + +```markup + +``` + + + + ```css button { @@ -127,20 +157,11 @@ button { - - -```markup - -``` - - - - + ```javascript export default function (component) { const { setTriggerValue, parentElement } = component; - parentElement.querySelector("button").onclick = () => { setTriggerValue("action", "button_clicked"); }; @@ -184,31 +205,30 @@ To avoid repeat warnings about re-registering the component, you can register yo -## Rich data exchange +## Rich data component -This example shows how to pass different data types to your component. It shows the following key concepts: +Streamlit will automatically serialize various data types to JSON or Arrow format. This example shows how to pass different data types to your component and simply display it. It shows the following key concepts: - Automatic dataframe conversion to Arrow format. - Passing JSON data. - Passing an image as a base64 string. - Accessing data in JavaScript via the destructured `data` property. +- Dynamically updating a placeholder element with the data. -`my_component/my_html.html`: +`my_component/component.html`: ```markup
Loading data...
``` -`my_component/my_js.js`: +`my_component/component.js`: ```javascript 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}

@@ -233,7 +253,8 @@ def create_sample_df(): "city": ["New York", "London", "Tokyo"] }) df = create_sample_df() -# Load an image and convert to bytes + +# 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: @@ -248,11 +269,11 @@ chart_component = st.components.v2.component( js=JS, ) -result = chart_component( +chart_component( data={ - "df": df, # Arrow-serializable dataframe - "user_info": {"name": "Alice"}, # JSON-serializable data - "image_base64": img_base64 # Image as base64 string + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64, # Image as base64 string } ) ``` @@ -272,7 +293,8 @@ def create_sample_df(): "city": ["New York", "London", "Tokyo"] }) df = create_sample_df() -# Load an image and convert to bytes + +# 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: @@ -301,24 +323,25 @@ chart_component = st.components.v2.component( """, ) -result = chart_component( +chart_component( data={ - "df": df, # Arrow-serializable dataframe - "user_info": {"name": "Alice"}, # JSON-serializable data - "image_base64": img_base64 # Image as base64 string + "df": df, # Arrow-serializable dataframe + "user_info": {"name": "Alice"}, # JSON-serializable data + "image_base64": img_base64, # Image as base64 string } ) ```
-## Complete interactive counter + -This comprehensive example demonstrates both state and trigger values. It shows the following key concepts: +## Interactive counter component -- Using state and trigger values together in one component. -- Using CSS custom properties to style the component. -- Bidirectional communication between Python and JavaScript. +This example shows a counter component that can be incremented, decremented, and reset. It contains multiple event handlers that are cleaned up when the component is unmounted. It shows the following key concepts: + +- State and trigger values together in one component. +- More comprehensive CSS custom properties to match the app's theme. - Multiple event handlers. - Cleanup functions for proper resource management. @@ -345,9 +368,11 @@ This comprehensive example demonstrates both state and trigger values. It shows font-family: var(--st-font); text-align: center; } + .buttons { margin-top: 15px; } + button { margin: 0 5px; padding: 8px 16px; @@ -357,9 +382,11 @@ button { border-radius: var(--st-button-radius); cursor: pointer; } + button:hover { opacity: 0.8; } + #reset { background: var(--st-red-color); } @@ -546,11 +573,14 @@ if result.reset:
-## Form with validation + -This example shows a more complex component with form validation. It shows the following key concepts: +## Danger button component + +You can include frontend validation processes in your component. This example shows 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("confirmed", true)`. The component also displays a progress ring to indicate the user's progress. + +- Frontend validation logic to gatekeep event submission. -- Form handling and validation. - Draft saving functionality. - Multiple event handlers and callbacks. - Using CSS custom properties to style the component. @@ -560,172 +590,325 @@ This example shows a more complex component with form validation. It shows the f `my_component/my_html.html`: ```markup -
-

Contact Form

-
- - - -
- - -
+
+
+ ⚠️ + Danger Zone +
+ + + +

Press and hold for 2 seconds to confirm

``` `my_component/my_css.css`: ```css -.form-container { - padding: 1rem; - border: 1px solid var(--st-border-color); +.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); - box-sizing: border-box; } -h3 { - font-size: var(--st-heading-font-size-h3, inherit); - font-weight: var(--st-heading-font-weight-h3, inherit); - margin: 0; + +.warning-icon { + font-size: 1rem; } -input, -textarea { - width: 100%; - padding: 0.5rem; - margin: 0.5rem 0; + +.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); - border: 1px solid transparent; - border-radius: var(--st-base-radius); - box-sizing: border-box; - font-size: inherit; - font-family: inherit; + 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); } -input:focus, -textarea:focus { - outline: none; - border-color: var(--st-primary-color); + +.hold-button:active:not(:disabled) { + transform: scale(0.98); } -textarea { - height: 5rem; - resize: vertical; + +.hold-button:disabled { + cursor: not-allowed; + opacity: 0.9; } -.form-actions { + +.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; - gap: 1rem; - margin-top: 0.75rem; + flex-direction: column; + align-items: center; + gap: 0.25rem; } -button { - padding: 0.5rem 1rem; - border-radius: var(--st-button-radius); - border: 1px solid transparent; - font-size: inherit; - font-family: inherit; + +.icon { + font-size: 2rem; + transition: transform 0.3s ease; } -button[type="submit"] { - background: var(--st-primary-color); - color: white; + +.hold-button:hover .icon { + transform: scale(1.1); } -button[type="button"] { - border: 1px solid var(--st-border-color); - background: var(--st-primary-background-color); + +.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; } -button:hover { - opacity: 0.9; - border-color: var(--st-primary-color); + +.hold-button.holding .label { + color: var(--st-red-color); + opacity: 1; } -#status { - margin-top: 0.5rem; + +.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; } ``` `my_component/my_js.js`: ```javascript -export default function ({ - parentElement, - setStateValue, - setTriggerValue, - data, -}) { - const form = parentElement.querySelector("#contact-form"); - const nameInput = parentElement.querySelector("#name"); - const emailInput = parentElement.querySelector("#email"); - const messageInput = parentElement.querySelector("#message"); - const saveDraftBtn = parentElement.querySelector("#save-draft"); - const status = parentElement.querySelector("#status"); - - // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights - requestAnimationFrame(() => { - const container = parentElement.querySelector(".form-container"); - const headingSizes = getComputedStyle(form) - .getPropertyValue("--st-heading-font-sizes") - .trim(); - const headingWeights = getComputedStyle(form) - .getPropertyValue("--st-heading-font-weights") - .trim(); - const sizes = headingSizes.split(",").map((s) => s.trim()); - const weights = headingWeights.split(",").map((s) => s.trim()); - if (sizes[2] && container) { - container.style.setProperty("--st-heading-font-size-h3", sizes[2]); - } - if (weights[2] && container) { - container.style.setProperty("--st-heading-font-weight-h3", weights[2]); - } - }); - - // Load draft if available - const loadDraft = (draft) => { - nameInput.value = draft.name || ""; - emailInput.value = draft.email || ""; - messageInput.value = draft.message || ""; - }; +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 }) { + 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); + } + } - loadDraft(data?.draft || {}); - - // Save draft - const saveDraft = () => { - setStateValue("draft", { - name: nameInput.value, - email: emailInput.value, - message: messageInput.value, - }); - setTriggerValue("action", "save_draft"); - status.textContent = "Draft saved!"; - status.style.color = "var(--st-green-color)"; - setTimeout(() => (status.textContent = ""), 2000); - }; + function startHold() { + if (isDisabled) return; // Ignore if in cooldown + + startTime = Date.now(); + button.classList.add("holding"); + label.textContent = "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } - // Submit form - const submitForm = (e) => { + 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); + } + + function handleTouchStart(e) { e.preventDefault(); + startHold(); + } - if (!nameInput.value || !emailInput.value || !messageInput.value) { - status.textContent = "Please fill all fields"; - status.style.color = "var(--st-red-color)"; - return; - } - - status.textContent = "Message sent!"; - status.style.color = "var(--st-blue-color)"; - setTimeout(() => (status.textContent = ""), 2000); - setTriggerValue("submit", { - name: nameInput.value, - email: emailInput.value, - message: messageInput.value, - }); - loadDraft({}); - setStateValue("draft", {}); - }; + // Mouse events + button.addEventListener("mousedown", startHold); + button.addEventListener("mouseup", cancelHold); + button.addEventListener("mouseleave", cancelHold); - // Event listeners - only update on button clicks - saveDraftBtn.addEventListener("click", saveDraft); - form.addEventListener("submit", submitForm); + // Touch events for mobile + button.addEventListener("touchstart", handleTouchStart); + button.addEventListener("touchend", cancelHold); + button.addEventListener("touchcancel", cancelHold); return () => { - saveDraftBtn.removeEventListener("click", saveDraft); - form.removeEventListener("submit", submitForm); + if (animationFrame) cancelAnimationFrame(animationFrame); + + // Remove mouse event listeners + button.removeEventListener("mousedown", startHold); + button.removeEventListener("mouseup", cancelHold); + button.removeEventListener("mouseleave", cancelHold); + + // Remove touch event listeners + button.removeEventListener("touchstart", handleTouchStart); + button.removeEventListener("touchend", cancelHold); + button.removeEventListener("touchcancel", cancelHold); }; } ``` @@ -736,37 +919,37 @@ export default function ({ import streamlit as st from my_component import HTML, CSS, JS -form_component = st.components.v2.component( - "contact_form", +danger_button = st.components.v2.component( + name="hold_to_confirm", html=HTML, css=CSS, js=JS, ) -# Handle form actions -def handle_form_action(): - # Process submission - # if submission_failed: - # submission = st.session_state.message_form.submit - # st.session_state.message_form.draft=submission - pass - -# Use the component -form_state = st.session_state.get("message_form", {}) -result = form_component( - data={"draft": form_state.get("draft", {})}, - default={"draft": form_state.get("draft", {})}, - on_draft_change=lambda: None, - on_submit_change=handle_form_action, - key="message_form" -) +st.title("Hold-to-Confirm Button") +st.caption("A dangerous action that requires intentional confirmation") -if result.submit: - st.write("Message Submitted:") - result.submit -else: - st.write("Current Draft:") - result.draft +# 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 +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}") ``` @@ -774,202 +957,357 @@ else: ```python import streamlit as st -form_component = st.components.v2.component( - "contact_form", +danger_button = st.components.v2.component( + name="hold_to_confirm", html=""" -
-

Contact Form

-
- - - -
- - -
+
+
+ ⚠️ + Danger Zone +
+ + + +

Press and hold for 2 seconds to confirm

""", css=""" - .form-container { - padding: 1rem; - border: 1px solid var(--st-border-color); + .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); - box-sizing: border-box; } - h3 { - font-size: var(--st-heading-font-size-h3, inherit); - font-weight: var(--st-heading-font-weight-h3, inherit); - margin: 0; + + .warning-icon { + font-size: 1rem; } - input, - textarea { - width: 100%; - padding: 0.5rem; - margin: 0.5rem 0; + + .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); - border: 1px solid transparent; - border-radius: var(--st-base-radius); - box-sizing: border-box; - font-size: inherit; - font-family: inherit; + 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; } - input:focus, - textarea:focus { - outline: none; - border-color: var(--st-primary-color); + + .hold-button.holding { + animation: pulse 0.5s ease-in-out infinite; + border-color: var(--st-red-color); } - textarea { - height: 5rem; - resize: vertical; + + .hold-button.triggered { + animation: success-burst 0.6s ease-out forwards; } - .form-actions { + + @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; - gap: 1rem; - margin-top: 0.75rem; + flex-direction: column; + align-items: center; + gap: 0.25rem; } - button { - padding: 0.5rem 1rem; - border-radius: var(--st-button-radius); - border: 1px solid transparent; - font-size: inherit; - font-family: inherit; + + .icon { + font-size: 2rem; + transition: transform 0.3s ease; } - button[type="submit"] { - background: var(--st-primary-color); - color: white; + + .hold-button:hover .icon { + transform: scale(1.1); } - button[type="button"] { - border: 1px solid var(--st-border-color); - background: var(--st-primary-background-color); + + .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; } - button:hover { - opacity: 0.9; - border-color: var(--st-primary-color); + + .hold-button.holding .label { + color: var(--st-red-color); + opacity: 1; } - #status { - margin-top: 0.5rem; + + .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, - setStateValue, - setTriggerValue, - data, - }) { - const form = parentElement.querySelector("#contact-form"); - const nameInput = parentElement.querySelector("#name"); - const emailInput = parentElement.querySelector("#email"); - const messageInput = parentElement.querySelector("#message"); - const saveDraftBtn = parentElement.querySelector("#save-draft"); - const status = parentElement.querySelector("#status"); - - // Register custom CSS variables with third values from --st-heading-font-sizes and --st-heading-font-weights - requestAnimationFrame(() => { - const container = parentElement.querySelector(".form-container"); - const headingSizes = getComputedStyle(form) - .getPropertyValue("--st-heading-font-sizes") - .trim(); - const headingWeights = getComputedStyle(form) - .getPropertyValue("--st-heading-font-weights") - .trim(); - const sizes = headingSizes.split(",").map((s) => s.trim()); - const weights = headingWeights.split(",").map((s) => s.trim()); - if (sizes[2] && container) { - container.style.setProperty("--st-heading-font-size-h3", sizes[2]); - } - if (weights[2] && container) { - container.style.setProperty("--st-heading-font-weight-h3", weights[2]); + 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 }) { + 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); } - }); + } - // Load draft if available - const loadDraft = (draft) => { - nameInput.value = draft.name || ""; - emailInput.value = draft.email || ""; - messageInput.value = draft.message || ""; - }; + function startHold() { + if (isDisabled) return; // Ignore if in cooldown - loadDraft(data?.draft || {}); - - // Save draft - const saveDraft = () => { - setStateValue("draft", { - name: nameInput.value, - email: emailInput.value, - message: messageInput.value, - }); - setTriggerValue("action", "save_draft"); - status.textContent = "Draft saved!"; - status.style.color = "var(--st-green-color)"; - setTimeout(() => (status.textContent = ""), 2000); - }; + startTime = Date.now(); + button.classList.add("holding"); + label.textContent = "Keep holding..."; + animationFrame = requestAnimationFrame(updateProgress); + } - // Submit form - const submitForm = (e) => { - e.preventDefault(); + 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 (!nameInput.value || !emailInput.value || !messageInput.value) { - status.textContent = "Please fill all fields"; - status.style.color = "var(--st-red-color)"; - return; + 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); + } + + function handleTouchStart(e) { + e.preventDefault(); + startHold(); + } - status.textContent = "Message sent!"; - status.style.color = "var(--st-blue-color)"; - setTimeout(() => (status.textContent = ""), 2000); - setTriggerValue("submit", { - name: nameInput.value, - email: emailInput.value, - message: messageInput.value, - }); - loadDraft({}); - setStateValue("draft", {}); - }; + // Mouse events + button.addEventListener("mousedown", startHold); + button.addEventListener("mouseup", cancelHold); + button.addEventListener("mouseleave", cancelHold); - // Event listeners - only update on button clicks - saveDraftBtn.addEventListener("click", saveDraft); - form.addEventListener("submit", submitForm); + // Touch events for mobile + button.addEventListener("touchstart", handleTouchStart); + button.addEventListener("touchend", cancelHold); + button.addEventListener("touchcancel", cancelHold); return () => { - saveDraftBtn.removeEventListener("click", saveDraft); - form.removeEventListener("submit", submitForm); + if (animationFrame) cancelAnimationFrame(animationFrame); + + // Remove mouse event listeners + button.removeEventListener("mousedown", startHold); + button.removeEventListener("mouseup", cancelHold); + button.removeEventListener("mouseleave", cancelHold); + + // Remove touch event listeners + button.removeEventListener("touchstart", handleTouchStart); + button.removeEventListener("touchend", cancelHold); + button.removeEventListener("touchcancel", cancelHold); }; } - """ + """, ) -# Handle form actions -def handle_form_action(): - # Process submission - # if submission_failed: - # submission = st.session_state.message_form.submit - # st.session_state.message_form.draft=submission - pass - -# Use the component -form_state = st.session_state.get("message_form", {}) -result = form_component( - data={"draft": form_state.get("draft", {})}, - default={"draft": form_state.get("draft", {})}, - on_draft_change=lambda: None, - on_submit_change=handle_form_action, - key="message_form" -) +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="⚠️") -if result.submit: - st.write("Message Submitted:") - result.submit -else: - st.write("Current Draft:") - result.draft + +# 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}") ``` + + ## What's next? Now that you've seen these examples: From 2bf6d171fb868b3cf8781ae285260e11019da59b Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Wed, 17 Dec 2025 09:45:15 -0800 Subject: [PATCH 22/26] Temp urls --- .../components-v2/quickstart.md | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 6b2778fe9..a6b087e59 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -43,7 +43,7 @@ hello_component = st.components.v2.component( hello_component() ``` - + ## Simple button component @@ -90,7 +90,7 @@ if result.action: st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") ``` - + For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you must read them into strings. The previous example is equivalent to the following: @@ -334,7 +334,7 @@ chart_component( - + ## Interactive counter component @@ -573,18 +573,15 @@ if result.reset: - + ## Danger button component You can include frontend validation processes in your component. This example shows 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("confirmed", true)`. The component also displays a progress ring to indicate the user's progress. - Frontend validation logic to gatekeep event submission. - -- Draft saving functionality. -- Multiple event handlers and callbacks. -- Using CSS custom properties to style the component. -- Session state integration for more complex, bidirectional state management. +- CSS custom properties to match the app's theme. +- Progress ring to indicate the user's progress. - Cleanup functions for proper resource management. `my_component/my_html.html`: @@ -1306,7 +1303,7 @@ if st.session_state.deleted_items: - + ## What's next? From 54df5b98d456ee00f3c23dc57fa6621b65e95473 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Sat, 20 Dec 2025 15:05:35 -0800 Subject: [PATCH 23/26] Update code format --- .../components-v2/quickstart.md | 277 +++++++++--------- 1 file changed, 140 insertions(+), 137 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index a6b087e59..9af773443 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -31,7 +31,7 @@ You can use custom components v2 to create static HTML and CSS components. The f - Mounting a component with its command created from registration. - Styling the component with the app's theme. -```python +```python filename="streamlit_app.py" import streamlit as st hello_component = st.components.v2.component( @@ -47,31 +47,33 @@ hello_component() ## Simple button component -Your v2 component can set user data to your app. This example shows a simple button that sends a trigger value to your app when clicked. Trigger values are one-time events that are not persisted across reruns. This example shows the following key concepts: +Your v2 component can send user data to your app. This example shows a simple button that sends a trigger value to your app when clicked. Trigger values are one-time events that are not persisted across reruns. This example shows the following key concepts: - Component registration with HTML, CSS, and JavaScript. - Trigger values using `setTriggerValue()`. - Callback functions with the `on__change` naming pattern. -```python +```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( - "interactive_button", + "simple_button", html="""""", css=""" button { - border: none; - padding: .5rem; - border-radius: var(--st-button-radius); - background-color: var(--st-primary-color); - color: white; + border: none; + padding: .5rem; + border-radius: var(--st-button-radius); + background-color: var(--st-primary-color); + color: white; } """, js=""" @@ -94,7 +96,7 @@ if result.action: For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you must read them into strings. The previous example is equivalent to the following: -``` +```none hideHeader project_directory/ ├── my_component/ │ ├── __init__.py @@ -106,7 +108,7 @@ project_directory/ -```python +```python filename="my_component/__init__.py" import streamlit as st from pathlib import Path @@ -137,7 +139,7 @@ JS = load_js() -```markup +```markup filename="my_component/component.html" ``` @@ -145,7 +147,7 @@ JS = load_js() -```css +```css filename="my_component/component.css" button { border: none; padding: 0.5rem; @@ -159,7 +161,7 @@ button { -```javascript +```javascript filename="my_component/component.js" export default function (component) { const { setTriggerValue, parentElement } = component; parentElement.querySelector("button").onclick = () => { @@ -172,7 +174,7 @@ export default function (component) { -```python +```python filename="streamlit_app.py" import streamlit as st from my_component import HTML, CSS, JS @@ -199,11 +201,11 @@ if result.action: The remaining examples on this page will use this file structure for easier viewing of the embedded code blocks. The complete code is provided at the end of each example for easier copying and pasting. - + -To avoid repeat warnings about re-registering the component, you can register your component in another module and import it. The standalone examples on this page are simple enough that this issue isn't apparent, but with more complex apps and components, this can be a nuisance. +If you are developing a component, temporarily remove `@st.cache_data` decorators to avoid manually clearing the cache when you make changes to the component. - + ## Rich data component @@ -215,31 +217,25 @@ Streamlit will automatically serialize various data types to JSON or Arrow forma - Accessing data in JavaScript via the destructured `data` property. - Dynamically updating a placeholder element with the data. -`my_component/component.html`: - -```markup +```markup filename="my_component/component.html"
Loading data...
``` -`my_component/component.js`: - -```javascript +```javascript filename="my_component/component.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}

- - `; +

Dataframe: ${df}

+

User Info: ${userInfo.name}

+ + `; } ``` -`streamlit_app.py`: - -```python +```python filename="streamlit_app.py" import pandas as pd import streamlit as st import base64 @@ -248,10 +244,13 @@ from my_component import HTML, JS # Create sample data @st.cache_data def create_sample_df(): - return pd.DataFrame({ - "name": ["Alice", "Bob", "Charlie"], - "city": ["New York", "London", "Tokyo"] -}) + return pd.DataFrame( + { + "name": ["Alice", "Bob", "Charlie"], + "city": ["New York", "London", "Tokyo"], + } + ) + df = create_sample_df() # Load an image and convert to b64 string @@ -260,6 +259,7 @@ 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 @@ -280,7 +280,7 @@ chart_component( -```python +```python filename="streamlit_app.py" import pandas as pd import streamlit as st import base64 @@ -288,10 +288,13 @@ import base64 # Create sample data @st.cache_data def create_sample_df(): - return pd.DataFrame({ - "name": ["Alice", "Bob", "Charlie"], - "city": ["New York", "London", "Tokyo"] -}) + return pd.DataFrame( + { + "name": ["Alice", "Bob", "Charlie"], + "city": ["New York", "London", "Tokyo"], + } + ) + df = create_sample_df() # Load an image and convert to b64 string @@ -300,6 +303,7 @@ 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 @@ -308,17 +312,15 @@ chart_component = st.components.v2.component( 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}

- - `; + 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}

+ + `; } """, ) @@ -338,31 +340,27 @@ chart_component( ## Interactive counter component -This example shows a counter component that can be incremented, decremented, and reset. It contains multiple event handlers that are cleaned up when the component is unmounted. It shows the following key concepts: +Your v2 component can maintain stateful values, either alone or in combination with trigger values. This example shows a counter component that can be incremented, decremented, and reset. Because it contains event handlers that aren't properties of the component object, they must be cleaned up when the component is unmounted. This example shows the following key concepts: - State and trigger values together in one component. - More comprehensive CSS custom properties to match the app's theme. - Multiple event handlers. -- Cleanup functions for proper resource management. +- Cleanup functions to remove event listeners when the component is unmounted. -`my_component/my_html.html`: - -```markup +```markup filename="my_component/component.html"
-

Count: 0

-
- - - -
+

Count: 0

+
+ + + +
``` -`my_component/my_css.css`: - -```css +```css filename="my_component/component.css" .counter { - padding: 20px; + padding: 2rem; border: 1px solid var(--st-border-color); border-radius: var(--st-base-radius); font-family: var(--st-font); @@ -370,12 +368,12 @@ This example shows a counter component that can be incremented, decremented, and } .buttons { - margin-top: 15px; + margin-top: 1rem; } button { - margin: 0 5px; - padding: 8px 16px; + margin: 0 0.5rem; + padding: 0.5rem 1rem; background: var(--st-primary-color); color: white; border: none; @@ -392,9 +390,7 @@ button:hover { } ``` -`my_component/my_js.js`: - -```javascript +```javascript filename="my_component/component.js" export default function ({ parentElement, setStateValue, @@ -440,9 +436,7 @@ export default function ({ } ``` -`streamlit_app.py`: - -```python +```python filename="streamlit_app.py" import streamlit as st from my_component import HTML, CSS, JS @@ -471,7 +465,7 @@ if result.reset: -```python +```python filename="streamlit_app.py" import streamlit as st # Interactive counter with both state and triggers @@ -479,81 +473,90 @@ counter = st.components.v2.component( "interactive_counter", html="""
-

Count: 0

-
- - - -
+

Count: 0

+
+ + + +
""", css=""" .counter { - padding: 20px; - border: 1px solid var(--st-border-color); - border-radius: var(--st-base-radius); - font-family: var(--st-font); - text-align: center; + 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: 15px; + margin-top: 1rem; } + button { - margin: 0 5px; - padding: 8px 16px; - background: var(--st-primary-color); - color: white; - border: none; - border-radius: var(--st-button-radius); - cursor: pointer; + 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; + opacity: 0.8; } + #reset { - background: var(--st-red-color); + 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 - }; + 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(); + }; - // Initialize + decrementBtn.onclick = () => { + count--; updateDisplay(); + }; - // Cleanup function - return () => { - incrementBtn.removeEventListener("click", incrementBtn.onclick); - decrementBtn.removeEventListener("click", decrementBtn.onclick); - resetBtn.removeEventListener("click", resetBtn.onclick); - }; + resetBtn.onclick = () => { + count = 0; + updateDisplay(); + setTriggerValue("reset", true); // One-time trigger + }; + + // Initialize + updateDisplay(); + + // Cleanup function + return () => { + incrementBtn.removeEventListener("click", incrementBtn.onclick); + decrementBtn.removeEventListener("click", decrementBtn.onclick); + resetBtn.removeEventListener("click", resetBtn.onclick); + }; } - """ + """, ) # Use with callbacks @@ -577,12 +580,12 @@ if result.reset: ## Danger button component -You can include frontend validation processes in your component. This example shows 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("confirmed", true)`. The component also displays a progress ring to indicate the user's progress. +You can include frontend validation processes in your component. This example shows 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 send the trigger value with `setTriggerValue("confirmed", true)`. The component gives the user visual feedback via a progress ring. This example shows the following key concepts: -- Frontend validation logic to gatekeep event submission. -- CSS custom properties to match the app's theme. -- Progress ring to indicate the user's progress. -- Cleanup functions for proper resource management. +- Frontend logic to validate user submissions before sending them to your app. +- Timed disablement to rate-limit user submissions. +- Visual feedback to the user. +- Cleanup functions to remove event listeners when the component is unmounted. `my_component/my_html.html`: From 24d4c730582184240d7dc8091949f480babe1146 Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Mon, 22 Dec 2025 13:23:47 -0800 Subject: [PATCH 24/26] Update danger button quickstart --- .../components-v2/quickstart.md | 563 +++++++----------- .../components-v2/theming.md | 4 +- 2 files changed, 233 insertions(+), 334 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 9af773443..0f8dbc40f 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -587,69 +587,20 @@ You can include frontend validation processes in your component. This example sh - Visual feedback to the user. - Cleanup functions to remove event listeners when the component is unmounted. -`my_component/my_html.html`: - -```markup -
-
- ⚠️ - Danger Zone -
- - - -

Press and hold for 2 seconds to confirm

-
+```markup filename="my_component/my_html.html" + ``` -`my_component/my_css.css`: - -```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); -} - +```css filename="my_component/my_css.css" .hold-button { position: relative; width: 7.5rem; @@ -731,7 +682,7 @@ You can include frontend validation processes in your component. This example sh stroke-dasharray: 283; stroke-dashoffset: 283; transition: stroke-dashoffset 0.1s linear; - filter: drop-shadow(0 0 6px var(--st-red-color)); + filter: drop-shadow(0 0 0.5rem var(--st-red-color)); } .button-content { @@ -741,6 +692,7 @@ You can include frontend validation processes in your component. This example sh flex-direction: column; align-items: center; gap: 0.25rem; + font-family: var(--st-font); } .icon { @@ -788,23 +740,14 @@ You can include frontend validation processes in your component. This example sh color: var(--st-primary-color); opacity: 1; } - -.hint { - font-size: 0.7rem; - color: var(--st-text-color); - opacity: 0.5; - margin: 0; -} ``` -`my_component/my_js.js`: - -```javascript +```javascript filename="my_component/my_js.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 }) { +export default function ({ parentElement, setTriggerValue, data }) { const button = parentElement.querySelector("#danger-btn"); const progress = parentElement.querySelector("#ring-progress"); const icon = parentElement.querySelector("#icon"); @@ -836,7 +779,7 @@ export default function ({ parentElement, setTriggerValue }) { startTime = Date.now(); button.classList.add("holding"); - label.textContent = "Keep holding..."; + label.textContent = data?.continue ?? "Keep holding..."; animationFrame = requestAnimationFrame(updateProgress); } @@ -845,7 +788,7 @@ export default function ({ parentElement, setTriggerValue }) { startTime = null; button.classList.remove("holding"); - label.textContent = "Hold to Delete"; + label.textContent = data?.start ?? "Hold to Delete"; progress.style.strokeDashoffset = CIRCUMFERENCE; if (animationFrame) { @@ -865,7 +808,7 @@ export default function ({ parentElement, setTriggerValue }) { button.disabled = true; icon.textContent = "✓"; - label.textContent = "Deleted!"; + label.textContent = data?.completed ?? "Deleted!"; progress.style.strokeDashoffset = 0; // Send trigger to Python @@ -876,8 +819,8 @@ export default function ({ parentElement, setTriggerValue }) { button.classList.remove("triggered"); button.disabled = false; isDisabled = false; - icon.textContent = "🗑️"; - label.textContent = "Hold to Delete"; + icon.textContent = data?.icon ?? "🗑️"; + label.textContent = data?.start ?? "Hold to Delete"; progress.style.strokeDashoffset = CIRCUMFERENCE; }, COOLDOWN_DURATION); } @@ -891,6 +834,7 @@ export default function ({ parentElement, setTriggerValue }) { 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); @@ -904,6 +848,7 @@ export default function ({ parentElement, setTriggerValue }) { button.removeEventListener("mousedown", startHold); button.removeEventListener("mouseup", cancelHold); button.removeEventListener("mouseleave", cancelHold); + button.removeEventListener("contextmenu", cancelHold); // Remove touch event listeners button.removeEventListener("touchstart", handleTouchStart); @@ -913,18 +858,9 @@ export default function ({ parentElement, setTriggerValue }) { } ``` -`streamlit_app.py`: - -```python +```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, -) +from danger_button_component import danger_button st.title("Hold-to-Confirm Button") st.caption("A dangerous action that requires intentional confirmation") @@ -933,16 +869,22 @@ st.caption("A dangerous action that requires intentional confirmation") 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="⚠️") + st.toast("Item permanently deleted!", icon="🗑️") # Render the component -result = danger_button(key="danger_btn", on_confirmed_change=on_delete_confirmed) +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: @@ -954,213 +896,162 @@ if st.session_state.deleted_items: -```python +```python filename="streamlit_app.py" import streamlit as st 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); + 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); + transform: scale(1.05); + border-color: var(--st-red-color); } .hold-button:active:not(:disabled) { - transform: scale(0.98); + transform: scale(0.98); } .hold-button:disabled { - cursor: not-allowed; - opacity: 0.9; + cursor: not-allowed; + opacity: 0.9; } .hold-button.holding { - animation: pulse 0.5s ease-in-out infinite; - border-color: var(--st-red-color); + 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; + 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; - } + 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); - } + 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); + 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; + 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)); + 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; + 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; + font-size: 2rem; + transition: transform 0.3s ease; } .hold-button:hover .icon { - transform: scale(1.1); + transform: scale(1.1); } .hold-button.holding .icon { - animation: shake 0.15s ease-in-out infinite; + 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); - } + 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; + 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; + 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; + color: var(--st-primary-color); + opacity: 1; } """, js=""" @@ -1168,112 +1059,114 @@ danger_button = st.components.v2.component( const COOLDOWN_DURATION = 1500; // cooldown after trigger const CIRCUMFERENCE = 2 * Math.PI * 45; // circle circumference - 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"); + 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 + let startTime = null; + let animationFrame = null; + let isDisabled = false; // Prevent interaction during cooldown - function updateProgress() { - if (!startTime) return; + function updateProgress() { + if (!startTime) return; - const elapsed = Date.now() - startTime; - const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); - const offset = CIRCUMFERENCE * (1 - progressPercent); + const elapsed = Date.now() - startTime; + const progressPercent = Math.min(elapsed / HOLD_DURATION, 1); + const offset = CIRCUMFERENCE * (1 - progressPercent); - progress.style.strokeDashoffset = offset; + progress.style.strokeDashoffset = offset; - if (progressPercent >= 1) { - // Triggered! - triggerAction(); - } else { - animationFrame = requestAnimationFrame(updateProgress); - } + if (progressPercent >= 1) { + // Triggered! + triggerAction(); + } else { + animationFrame = requestAnimationFrame(updateProgress); } + } - function startHold() { - if (isDisabled) return; // Ignore if in cooldown + function startHold() { + if (isDisabled) return; // Ignore if in cooldown - startTime = Date.now(); - button.classList.add("holding"); - label.textContent = "Keep holding..."; - animationFrame = requestAnimationFrame(updateProgress); - } + 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 + function cancelHold() { + if (isDisabled) return; // Ignore if in cooldown - startTime = null; - button.classList.remove("holding"); - label.textContent = "Hold to Delete"; - progress.style.strokeDashoffset = CIRCUMFERENCE; + startTime = null; + button.classList.remove("holding"); + label.textContent = data?.start ?? "Hold to Delete"; + progress.style.strokeDashoffset = CIRCUMFERENCE; - if (animationFrame) { - cancelAnimationFrame(animationFrame); - animationFrame = null; - } + 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); - 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); - } - - function handleTouchStart(e) { - e.preventDefault(); - startHold(); - } - - // Mouse events - button.addEventListener("mousedown", startHold); - button.addEventListener("mouseup", cancelHold); - button.addEventListener("mouseleave", cancelHold); - - // 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); - - // Remove touch event listeners - button.removeEventListener("touchstart", handleTouchStart); - button.removeEventListener("touchend", cancelHold); - button.removeEventListener("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); + }; } """, ) @@ -1285,16 +1178,22 @@ st.caption("A dangerous action that requires intentional confirmation") 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="⚠️") + st.toast("Item permanently deleted!", icon="🗑️") # Render the component -result = danger_button(key="danger_btn", on_confirmed_change=on_delete_confirmed) +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: @@ -1313,6 +1212,6 @@ if st.session_state.deleted_items: Now that you've seen these examples: - Learn the fundamentals in [Create components](/develop/concepts/custom-components/components-v2/create). -- Understand [State vs triggers](/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. +- Understand [State versus trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) for advanced interactions. +- Explore [Component 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/theming.md b/content/develop/concepts/custom-components/components-v2/theming.md index 02f5e6b8c..772f6ed00 100644 --- a/content/develop/concepts/custom-components/components-v2/theming.md +++ b/content/develop/concepts/custom-components/components-v2/theming.md @@ -1,11 +1,11 @@ --- -title: Theming and styling +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 --- -# Theming and 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. From 6566ab1269beda9fa180a7890f87bbec14ceffeb Mon Sep 17 00:00:00 2001 From: Debbie Matthews Date: Mon, 22 Dec 2025 13:50:10 -0800 Subject: [PATCH 25/26] Show embedded app first in quickstart --- .../components-v2/quickstart.md | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index 0f8dbc40f..d3cd4a5b0 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -31,6 +31,8 @@ You can use custom components v2 to create static HTML and CSS components. The f - Mounting a component with its command created from registration. - Styling the component with the app's theme. + + ```python filename="streamlit_app.py" import streamlit as st @@ -43,8 +45,6 @@ hello_component = st.components.v2.component( hello_component() ``` - - ## Simple button component Your v2 component can send user data to your app. This example shows a simple button that sends a trigger value to your app when clicked. Trigger values are one-time events that are not persisted across reruns. This example shows the following key concepts: @@ -53,6 +53,8 @@ Your v2 component can send user data to your app. This example shows a simple bu - Trigger values using `setTriggerValue()`. - Callback functions with the `on__change` naming pattern. + + ```python filename="streamlit_app.py" import streamlit as st @@ -92,8 +94,6 @@ if result.action: st.write(f"Button clicked! Total clicks: {st.session_state.click_count}") ``` - - For inline component development, you must pass raw HTML, CSS, and JavaScript code to your component. Package-based components allow you to pass file references to your component. If you want to use files for an inline component, you must read them into strings. The previous example is equivalent to the following: ```none hideHeader @@ -217,6 +217,8 @@ Streamlit will automatically serialize various data types to JSON or Arrow forma - Accessing data in JavaScript via the destructured `data` property. - Dynamically updating a placeholder element with the data. + + ```markup filename="my_component/component.html"
Loading data...
``` @@ -336,8 +338,6 @@ chart_component(
- - ## Interactive counter component Your v2 component can maintain stateful values, either alone or in combination with trigger values. This example shows a counter component that can be incremented, decremented, and reset. Because it contains event handlers that aren't properties of the component object, they must be cleaned up when the component is unmounted. This example shows the following key concepts: @@ -347,6 +347,8 @@ Your v2 component can maintain stateful values, either alone or in combination w - Multiple event handlers. - Cleanup functions to remove event listeners when the component is unmounted. + + ```markup filename="my_component/component.html"

Count: 0

@@ -576,8 +578,6 @@ if result.reset: - - ## Danger button component You can include frontend validation processes in your component. This example shows 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 send the trigger value with `setTriggerValue("confirmed", true)`. The component gives the user visual feedback via a progress ring. This example shows the following key concepts: @@ -585,7 +585,9 @@ You can include frontend validation processes in your component. This example sh - Frontend logic to validate user submissions before sending them to your app. - Timed disablement to rate-limit user submissions. - Visual feedback to the user. -- Cleanup functions to remove event listeners when the component is unmounted. +- Using `width` with Streamlit's layout system. + + ```markup filename="my_component/my_html.html" ", @@ -40,14 +41,11 @@ my_component = st.components.v2.component( ### Registration parameters -- `name` (required) is a unique identifier for your component type. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code. Avoid registering multiple components with the same name. -- `html` (optional) is the HTML markup for your component. It defines the visual structure of your component. In the previous example, this is a single HTML button element. -- `css` (optional) is the CSS styling for your component. In the previous example, the CSS sets the button's background color to the primary color from the Streamlit theme and sets the text color to white. -- `js` (optional) is the JavaScript logic for your component. In the previous example, the JavaScript listens for a click event on the button and sets the `clicked` trigger value to `true`. +`name` is a unique identifier for your component. This is used internally by Streamlit for each instance to retrieve its HTML, CSS, and JavaScript code when an 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. -For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. File references are only supported for package-based components. +`html`, `css`, and `js` are all optional parameters that define your component's markup, styling, and logic, respectively. In the previous example, `html` contains a single button element, `css` styles it with the Streamlit theme's primary color, and `js` listens for clicks and sets a trigger value. -When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend. For a package-based component, Streamlit serves the contents of the package's asset directory, which makes those resources available to your app's frontend and accessible through relative paths. Streamlit doesn't serve an asset directory for inline components. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). +For inline component development, the HTML, CSS, and JavaScript code must be raw code as strings. For a package-based component, Streamlit serves the component's assets in a declared asset directory. This makes the component's resources available to your app's frontend and accessible through frontend paths relative to the asset directory. When you use a path in the `st.components.v2.component()` call, the paths are resolved on the frontend and therefore rely on the presence of the asset directory. Streamlit doesn't serve an asset directory for inline components. For more information, see [Package-based components](/develop/concepts/custom-components/components-v2/package-based). @@ -70,7 +68,7 @@ export default function (component) { } ``` -The `component` parameter provides these essential properties as documented in the [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) type. These properties are typically destructured into local variables for easier access. +The `component` argument in your default export function provides essential properties as documented 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) { @@ -83,7 +81,7 @@ export default function (component) { - `name` (string): Component name from your Python registration. - `key` (string): Unique identifier for this 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. +- `data` (any): All data passed from Python via the `data` parameter. Use this to customize a component instance or to create a feedback loop with your Python code. - `parentElement` (HTMLElement): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements. - `setStateValue` (function): JS 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): JS 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. @@ -101,7 +99,7 @@ Don't directly overwrite or replace `parentElement.innerHTML`. If you do, you wi #### Simple HTML component -In the following examples, we'll register a simple component that displays "Hello, World!" in a heading. We use the primary color from the Streamlit theme for the heading color. For more information about making your components theme-aware, see the [Theming & styling](/develop/concepts/custom-components/components-v2/theming) guide. This example is completed at the end of this guide in the [Complete examples](#simple-html-component-complete-example) section. +In the following examples, we'll register a simple component that displays "Hello, World!" in a heading. We use the primary color from the Streamlit theme for the heading color. For more information about making your components theme-aware, see the [Theming and styling](/develop/concepts/custom-components/components-v2/theming) guide. This example is completed at the end of this guide in the [Complete examples](#simple-html-component-complete-example) section. ```python import streamlit as st @@ -119,11 +117,11 @@ For larger components, you can organize your code into separate files. However, ``` my_app/ -├── streamlit_app.py # Entrypoint file +├── streamlit_app.py └── my_component/ - ├── my_css.css # Component styles - ├── my_html.html # Component HTML - └── my_js.js # Component JavaScript + ├── component.css + ├── component.html + └── component.js ``` ```python @@ -148,6 +146,12 @@ file_component = st.components.v2.component( ) ``` + + +Using `@st.cache_data` is a good practice to avoid reloading the component code on every rerun, but you might want to disable it temporarily during development. Streamlit will automatically invalidate the cacne if you make code changes within a cache-decorated function, but Streamlit can't infer the changes from files that are read. For more information, see [Caching](/develop/api-reference/caching). + + + #### Interactive component In the following example, we'll register a component that displays a counter and a button to increment the counter. The counter value is stored in the component's state and is updated when the button is clicked. The component also triggers an event when the button is clicked. The component properties are destructured within the function signature directly. This example is completed at the end of this guide in the [Complete examples](#interactive-counter-complete-example) section. diff --git a/content/develop/concepts/custom-components/components-v2/quickstart.md b/content/develop/concepts/custom-components/components-v2/quickstart.md index d3cd4a5b0..00edc2792 100644 --- a/content/develop/concepts/custom-components/components-v2/quickstart.md +++ b/content/develop/concepts/custom-components/components-v2/quickstart.md @@ -13,13 +13,13 @@ Get started with custom components v2 through these practical examples that demo Creating and using a custom component involves two distinct steps: -1. Register your component to define its structure (HTML, CSS, JavaScript). - - Register a component with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). +1. **Registration** + - Define your component's HTML, CSS, and JavaScript with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component). - Optional: To enable bidirectional communication, within your component's JavaScript function, communicate with Python by calling `setStateValue()` or `setTriggerValue()`. These are properties of the [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) object passed to your function. - Optional: To make your component theme-aware, within your component's CSS, style your component with Streamlit's [theme variables](/develop/concepts/custom-components/components-v2/theming#using-css-custom-properties). -2. Mount your component to create a specific instance in your app. - - Use your component command, which inherits from the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) class. +2. **Mounting** + - To create a specific instance of your component in your app, use your component command, which inherits from the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) class. For more information, see [Create custom v2 components](/develop/concepts/custom-components/components-v2/create).