diff --git a/content/develop/concepts/custom-components/_index.md b/content/develop/concepts/custom-components/_index.md
index fd0c24a3c..0585ba456 100644
--- a/content/develop/concepts/custom-components/_index.md
+++ b/content/develop/concepts/custom-components/_index.md
@@ -1,62 +1,52 @@
---
-title: Components
+title: Custom Components
slug: /develop/concepts/custom-components
-description: Learn how to build and use custom Streamlit components to extend app functionality with third-party Python modules and custom UI elements.
-keywords: custom components, third-party modules, component development, extend functionality, custom UI, component integration, Streamlit components
+description: Learn about Streamlit custom components - powerful extensions that unlock capabilities beyond built-in widgets using web technologies.
+keywords: custom components, component development, extend streamlit, web components, custom widgets, component architecture
---
# Custom Components
-Components are third-party Python modules that extend what's possible with Streamlit.
+Custom Components are powerful extensions for Streamlit that unlock capabilities beyond the built-in widgets. They let you integrate any web technology—from advanced data visualizations to specialized input controls to complete mini-applications—directly into your Streamlit apps.
-## How to use a Component
+## Getting started
-Components are super easy to use:
+
-1. Start by finding the Component you'd like to use. Two great resources for this are:
- - The [Component gallery](https://streamlit.io/components)
- - [This thread](https://discuss.streamlit.io/t/streamlit-components-community-tracker/4634),
- by Fanilo A. from our forums.
+
-2. Install the Component using your favorite Python package manager. This step and all following
- steps are described in your component's instructions.
+
Overview of Custom Components
- For example, to use the fantastic [AgGrid
- Component](https://github.com/PablocFonseca/streamlit-aggrid), you first install it with:
+Learn what custom components are, when to use them, and understand the differences between v1 and v2 approaches.
- ```python
- pip install streamlit-aggrid
- ```
+
-3. In your Python code, import the Component as described in its instructions. For AgGrid, this step
- is:
+
- ```python
- from st_aggrid import AgGrid
- ```
+
Components v2
-4. ...now you're ready to use it! For AgGrid, that's:
+The next generation of custom components with enhanced capabilities, bidirectional communication, and simplified development.
- ```python
- AgGrid(my_dataframe)
- ```
+
-## Making your own Component
+
-If you're interested in making your own component, check out the following resources:
+
Components v1
-- [Create a Component](/develop/concepts/custom-components/create)
-- [Publish a Component](/develop/concepts/custom-components/publish)
-- [Components API](/develop/concepts/custom-components/intro)
-- [Blog post for when we launched Components!](https://blog.streamlit.io/introducing-streamlit-components/)
+The original custom components framework. Learn how to use and build v1 components.
-Alternatively, if you prefer to learn using videos, our engineer Tim Conkling has put together some
-amazing tutorials:
+
-##### Video tutorial, part 1
+
-
+
Publishing Components
-##### Video tutorial, part 2
+Learn how to package and distribute your custom components to the community.
-
+
+
+
+
+## Component gallery
+
+Explore the [Community Component Gallery](https://streamlit.io/components) to discover components built by the Streamlit community.
diff --git a/content/develop/concepts/custom-components/components-v1/_index.md b/content/develop/concepts/custom-components/components-v1/_index.md
new file mode 100644
index 000000000..f9a64644b
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v1/_index.md
@@ -0,0 +1,62 @@
+---
+title: Components v1
+slug: /develop/concepts/custom-components/components-v1
+description: Learn how to build and use custom Streamlit components to extend app functionality with third-party Python modules and custom UI elements.
+keywords: custom components, third-party modules, component development, extend functionality, custom UI, component integration, Streamlit components
+---
+
+# Custom Components
+
+Components are third-party Python modules that extend what's possible with Streamlit.
+
+## How to use a Component
+
+Components are super easy to use:
+
+1. Start by finding the Component you'd like to use. Two great resources for this are:
+ - The [Component gallery](https://streamlit.io/components)
+ - [This thread](https://discuss.streamlit.io/t/streamlit-components-community-tracker/4634),
+ by Fanilo A. from our forums.
+
+2. Install the Component using your favorite Python package manager. This step and all following
+ steps are described in your component's instructions.
+
+ For example, to use the fantastic [AgGrid
+ Component](https://github.com/PablocFonseca/streamlit-aggrid), you first install it with:
+
+ ```python
+ pip install streamlit-aggrid
+ ```
+
+3. In your Python code, import the Component as described in its instructions. For AgGrid, this step
+ is:
+
+ ```python
+ from st_aggrid import AgGrid
+ ```
+
+4. ...now you're ready to use it! For AgGrid, that's:
+
+ ```python
+ AgGrid(my_dataframe)
+ ```
+
+## Making your own Component
+
+If you're interested in making your own component, check out the following resources:
+
+- [Create a Component](/develop/concepts/custom-components/components-v1/create)
+- [Publish a Component](/develop/concepts/custom-components/publish)
+- [Components API](/develop/concepts/custom-components/components-v1/intro)
+- [Blog post for when we launched Components!](https://blog.streamlit.io/introducing-streamlit-components/)
+
+Alternatively, if you prefer to learn using videos, our engineer Tim Conkling has put together some
+amazing tutorials:
+
+##### Video tutorial, part 1
+
+
+
+##### Video tutorial, part 2
+
+
diff --git a/content/develop/concepts/custom-components/components-api.md b/content/develop/concepts/custom-components/components-v1/v1-component-api.md
similarity index 99%
rename from content/develop/concepts/custom-components/components-api.md
rename to content/develop/concepts/custom-components/components-v1/v1-component-api.md
index 6495ae67b..3c989cf28 100644
--- a/content/develop/concepts/custom-components/components-api.md
+++ b/content/develop/concepts/custom-components/components-v1/v1-component-api.md
@@ -1,6 +1,6 @@
---
title: Intro to custom components
-slug: /develop/concepts/custom-components/intro
+slug: /develop/concepts/custom-components/components-v1/intro
description: Learn to develop Streamlit custom components with static and bi-directional communication between Python and JavaScript for extended functionality.
keywords: custom component development, static components, bi-directional components, Python JavaScript communication, component API, component development
---
diff --git a/content/develop/concepts/custom-components/create-component.md b/content/develop/concepts/custom-components/components-v1/v1-component-create.md
similarity index 96%
rename from content/develop/concepts/custom-components/create-component.md
rename to content/develop/concepts/custom-components/components-v1/v1-component-create.md
index 9c4a77328..3ca20016d 100644
--- a/content/develop/concepts/custom-components/create-component.md
+++ b/content/develop/concepts/custom-components/components-v1/v1-component-create.md
@@ -1,6 +1,6 @@
---
title: Create a Component
-slug: /develop/concepts/custom-components/create
+slug: /develop/concepts/custom-components/components-v1/create
description: Step-by-step guide to creating custom Streamlit components from scratch, including setup, development environment, and component structure.
keywords: create component, component development, component setup, development environment, component structure, custom component creation, build components
---
diff --git a/content/develop/concepts/custom-components/limitations.md b/content/develop/concepts/custom-components/components-v1/v1-component-limitations.md
similarity index 96%
rename from content/develop/concepts/custom-components/limitations.md
rename to content/develop/concepts/custom-components/components-v1/v1-component-limitations.md
index f92c4df66..bf0fe8bbd 100644
--- a/content/develop/concepts/custom-components/limitations.md
+++ b/content/develop/concepts/custom-components/components-v1/v1-component-limitations.md
@@ -1,6 +1,6 @@
---
title: Limitations of custom components
-slug: /develop/concepts/custom-components/limitations
+slug: /develop/concepts/custom-components/components-v1/limitations
description: Understand the limitations and constraints of Streamlit custom components including iframe restrictions and differences from base Streamlit functionality.
keywords: component limitations, iframe restrictions, component constraints, custom component issues, component differences, development limitations
---
diff --git a/content/develop/concepts/custom-components/components-v2/_index.md b/content/develop/concepts/custom-components/components-v2/_index.md
new file mode 100644
index 000000000..415440641
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/_index.md
@@ -0,0 +1,83 @@
+---
+title: Custom components v2
+slug: /develop/concepts/custom-components/components-v2
+description: Learn about Streamlit custom components v2 - the next generation framework with enhanced capabilities, bidirectional communication, and simplified development.
+keywords: custom components v2, next generation components, bidirectional communication, enhanced capabilities, modern component development
+---
+
+# Custom components v2
+
+Components v2 represents a reimagining of how custom components work in Streamlit. It's designed to unlock new capabilities and dramatically simplify the development experience. To view the command reference, see the [API Reference](/develop/api-reference/custom-components).
+
+## Getting started
+
+
+
+
+
+
Quickstart examples
+
+Get started quickly with practical examples showing interactive buttons, data exchange, and complete component implementations.
+
+
+
+
+
+
Component registration
+
+Define your component's structure with HTML, CSS, and JavaScript.
+
+
+
+
+
+
Component mounting
+
+Create instances of your component in your app and handle their output.
+
+
+
+
+
+
Bidirectional communication
+
+Exchange data between your component and Python.
+
+
+
+
+
+
State vs triggers
+
+Understand the two communication mechanisms for building interactive components.
+
+
+
+
+
+
Theming and styling
+
+Make your components look great with Streamlit's theme integration and CSS custom properties.
+
+
+
+
+
+
Package-based components
+
+Build complex components with modern frontend tooling, TypeScript, and external dependencies.
+
+
+
+
+
+## Migration from v1 to v2
+
+If you have existing v1 components, check out these migration examples:
+
+- [streamlit-bokeh v2 migration](https://github.com/streamlit/streamlit-bokeh/pull/40)
+- [streamlit-pdf v2 migration](https://github.com/streamlit/streamlit-pdf/pull/25)
+
+## What's next?
+
+Ready to build your first v2 component? Start with the [Quickstart examples](/develop/concepts/custom-components/components-v2/examples) to see practical implementations, then learn about [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount) to understand the fundamentals.
diff --git a/content/develop/concepts/custom-components/components-v2/communicate.md b/content/develop/concepts/custom-components/components-v2/communicate.md
new file mode 100644
index 000000000..b1f414fb0
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/communicate.md
@@ -0,0 +1,140 @@
+---
+title: Bidirectional communication
+slug: /develop/concepts/custom-components/components-v2/communicate
+description: Learn how to exchange data between your custom v2 component and Python, including sending data to the frontend and receiving user interactions.
+keywords: custom components v2, bidirectional communication, data exchange, setStateValue, setTriggerValue, callbacks, data parameter
+---
+
+# Bidirectional communication
+
+Custom components v2 supports full bidirectional communication between your Python backend and JavaScript frontend. This enables you to:
+
+- Send data from Python to your component via the `data` parameter
+- Receive user actions in Python from your component's state and trigger values
+- Create feedback loops where Python updates the component programmatically
+
+The basic concepts of component communication are introduced in the [Component mounting](/develop/concepts/custom-components/components-v2/mount) guide. After you understand the basics, read this guide to learn how to create feedback loops where Python updates the component programmatically.
+
+
+
+This guide explains how to recreate behavior similar to that of native Streamlit widgets: setting a component's state from Session State. You don't strictly need feed a component's state from Session State back into itself to create a stateful component. This is just one pattern to achieve an experience that is similar to native Streamlit widgets.
+
+However, because components can have multiple states and triggers, you must work with `st.session_state..` for custom components instead of simply `st.session_state.` like you do with native Streamlit widgets.
+
+
+
+## Prerequisites
+
+Before you read this guide, you should understand the following concepts:
+
+- [Component mounting](/develop/concepts/custom-components/components-v2/mount)
+- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers)
+- [Widget behavior](/develop/concepts/architecture/widget-behavior)
+
+## The communication cycle
+
+Custom components communicate through a cycle:
+
+1. **Python → JavaScript**: Python sends data to your component via the `data` parameter.
+2. **User interaction**: The user interacts with your component in the browser.
+3. **JavaScript → Python**: Your component sends the result back via `setStateValue()` or `setTriggerValue()`.
+4. **Python updates**: All related callback functions are executed and the component's result is updated in a script rerun.
+
+For native Streamlit widgets, you can assign a key and set a widget's state through Session State. However, custom components don't automatically pass information from Session State to their associated component. To programmatically update a component from Python, you need to pass new data to the component's `data` parameter. If you want to set your component's state from Session State, you must pass the component's Session State values to the component's `data` parameter.
+
+## Creating a feedback loop
+
+Here's a text input component that demonstrates this pattern. This is the [text input](/develop/concepts/custom-components/components-v2/examples/text-input) component shown in the quickstart guide.
+
+
+
+### The JavaScript side
+
+Your component's JavaScript function must read `data` to initialize and update its state:
+
+```javascript
+export default function (component) {
+ const { setStateValue, parentElement, data } = component;
+
+ const label = parentElement.querySelector("label");
+ label.innerText = data.label;
+
+ const input = parentElement.querySelector("input");
+ // Sync input value with data from Python
+ if (input.value !== data.value) {
+ input.value = data.value ?? "";
+ }
+
+ input.onkeydown = (e) => {
+ if (e.key === "Enter") {
+ setStateValue("value", e.target.value);
+ }
+ };
+
+ input.onblur = (e) => {
+ setStateValue("value", e.target.value);
+ };
+}
+```
+
+The conditional expression (`if (input.value !== data.value)`) updates the input field when Python sends new data. Because the Python code (in the next section) sets the state value using the `default` parameter, the component doesn't need to use `setStateValue()` here.
+
+### The Python side
+
+In Python, you can create a wrapper function that reads from Session State and passes updated data:
+
+```python
+def my_component_wrapper(
+ label, *, default="", key=None, on_change=lambda: None
+):
+ # Read current state from Session State
+ if key is not None:
+ component_state = st.session_state.get(key, {})
+ value = component_state.get("value", default)
+ else:
+ value = default
+
+ # Pass current value back to component
+ data = {"label": label, "value": value}
+ result = my_component(
+ data=data,
+ default={"value": value},
+ key=key,
+ on_value_change=on_change,
+ )
+ return result
+```
+
+To create a clean mounting command, the wrapper lets you declare the component's label, initial value, key, and callback function. Within the
+wrapper, when a key is provided, use the `get()` method on `st.session_state` to read the current component state. This prevents an error on the first script run, before the component is mounted.
+
+The `get()` method is used twice. First, get the component state from its key. Component states are dictionaries of state and trigger values. In this case, the component has a single state named `"value"`. Then, from the component state, get the value of the the `"value"` state. If the `"value"` state isn't defined or no key is provided, use the provided default value.
+
+Finally, within the wrapper, call the raw mounting command, passing in the current data. You can directly pass through the `key` and `on_change` values. However, `data` and `default` are constructed from the previous logic that uses the existing component state.
+
+### Programmatic updates
+
+With this pattern, you can update the component from Python by modifying Session State:
+
+```python
+if st.button("Set greeting"):
+ st.session_state["my_text_input"]["value"] = "Hello World"
+
+if st.button("Clear"):
+ st.session_state["my_text_input"]["value"] = ""
+
+result = my_component_wrapper("Enter text", key="my_text_input")
+```
+
+When you click a button, it modifies Session State. On the rerun, the wrapper reads the new value from Session State and passes it to the component via `data`. The JavaScript sees the updated `data.value` and updates the input field.
+
+## Complete example
+
+See the [Text input component example](/develop/concepts/custom-components/components-v2/examples/text-input) for the full working code.
+
+## Key takeaways
+
+1. **`data` is one-way**: Python sends data to JavaScript, but changes in JavaScript don't automatically update Python.
+2. **State values bridge the gap**: Use `setStateValue()` to send user interactions back to Python.
+3. **Session State enables control**: Store component state with a `key`, then read from Session State to update `data`.
+4. **Sync carefully**: Check if the value has changed before updating DOM elements to avoid interfering with user input.
diff --git a/content/develop/concepts/custom-components/components-v2/examples/_index.md b/content/develop/concepts/custom-components/components-v2/examples/_index.md
new file mode 100644
index 000000000..6f8010e4d
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/_index.md
@@ -0,0 +1,386 @@
+---
+title: Quickstart examples
+slug: /develop/concepts/custom-components/components-v2/examples
+description: Get started quickly with Custom Components v2 through practical examples showing interactive buttons, data exchange, and complete component implementations.
+keywords: custom components v2, quickstart, examples, interactive components, data exchange, component examples, getting started
+---
+
+# Quickstart examples
+
+Get started with custom components v2 through these practical examples. Each example introduces a new concept to progressively build your understanding. To highlight each concept, the code on this page shows either a portion of the component example or a simplified version of it. Follow the links below each example to see the complete code for that example, including explanations.
+
+## Two-step component process
+
+Creating and using a custom component involves two distinct steps:
+
+1. **Registration**: Define your component's HTML, CSS, and JavaScript with [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component).
+2. **Mounting**: Mount a specific instance of your component to your app's frontend using the [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) created during registration.
+
+For detailed explanations, see [Component registration](/develop/concepts/custom-components/components-v2/register) and [Component mounting](/develop/concepts/custom-components/components-v2/mount).
+
+## Hello world
+
+This is a minimal static component that displays "Hello, World!" using the app's primary theme color. This component introduces the following concepts:
+
+- Component registration with HTML and CSS using `st.components.v2.component()`
+- Theme integration using CSS custom properties
+- Mounting a component by calling the `ComponentRenderer`
+
+
+
+```python filename="streamlit_app.py"
+import streamlit as st
+
+hello_component = st.components.v2.component(
+ name="hello_world",
+ html="
Hello, World!
",
+ css="h2 { color: var(--st-primary-color); }",
+)
+
+hello_component()
+```
+
+
+
+## Rich data
+
+This is a component that receives various data types from Python. This component introduces the following concepts:
+
+- Passing data from Python via the `data` parameter and accessing it in JavaScript
+- Automatic dataframe and JSON serialization
+- Passing an image as a Base64-encoded string
+- Using a placeholder in the component's HTML and dynamically updating it with received data
+
+
+
+```python
+data_component = st.components.v2.component(
+ "rich_data",
+ html="""
",
+ css="h2 { color: var(--st-primary-color); }",
+)
+
+hello_component()
+```
+
+## How it works
+
+### Registration
+
+The component is registered with the following parameters:
+
+- `name`: `"hello_world"` is a unique identifier that Streamlit uses internally to retrieve the component's HTML and CSS code when an instance of the component is mounted.
+- `html`: `"
Hello, World!
"` is the markup that Streamlit renders in the component's DOM.
+- `css`: `"h2 { color: var(--st-primary-color); }"` uses a CSS custom property to apply the app's primary color to the component's heading element.
+
+### Theming
+
+Most theme configuration options can be converted from camel case to dash-case and used as CSS custom properties. For example, `theme.primaryColor` becomes `--st-primary-color`. In this example, `var(--st-primary-color)` is used to apply the app's primary color to the component's heading. If an app has a light and dark theme configured, the CSS custom property will reflect the value of the current theme. For more theme variables, see [Theming and styling](/develop/concepts/custom-components/components-v2/theming).
+
+### Mounting
+
+The registration command returns a [`ComponentRenderer`](/develop/api-reference/custom-components/st.components.v2.types.componentrenderer) that can be called to mount an instance of the component. In this example, the component is mounted by calling `hello_component()`. Because this is a static component with no interactivity, no additional parameters are needed to mount the component.
diff --git a/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md b/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md
new file mode 100644
index 000000000..a3b3cf697
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/interactive-counter.md
@@ -0,0 +1,290 @@
+---
+title: "Component example: Interactive counter"
+slug: /develop/concepts/custom-components/components-v2/examples/interactive-counter
+description: A counter component demonstrating state values, trigger values, multiple event handlers, and cleanup functions.
+keywords: custom components v2, example, counter, state values, trigger values, cleanup function, event handlers
+---
+
+# Component example: Interactive counter
+
+This is a counter component that can be incremented, decremented, and reset. It demonstrates combining state values (persistent count) with trigger values (reset event).
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- Combining state and trigger values in one component
+- Multiple event handlers
+
+## Complete code
+
+For easy copying and pasting, exapand the complete code below. However, for easier reading, the code is split into multiple files in the next section.
+
+
+
+```python filename="streamlit_app.py"
+import streamlit as st
+
+# Interactive counter with both state and triggers
+counter = st.components.v2.component(
+ "interactive_counter",
+ html="""
+
+```
+
+```css filename="my_component/component.css"
+.counter {
+ padding: 2rem;
+ border: 1px solid var(--st-border-color);
+ border-radius: var(--st-base-radius);
+ font-family: var(--st-font);
+ text-align: center;
+}
+
+.buttons {
+ margin-top: 1rem;
+}
+
+button {
+ margin: 0 0.5rem;
+ padding: 0.5rem 1rem;
+ background: var(--st-primary-color);
+ color: white;
+ border: none;
+ border-radius: var(--st-button-radius);
+ cursor: pointer;
+}
+
+button:hover {
+ opacity: 0.8;
+}
+
+#reset {
+ background: var(--st-red-color);
+}
+```
+
+```javascript filename="my_component/component.js"
+export default function ({
+ parentElement,
+ setStateValue,
+ setTriggerValue,
+ data,
+}) {
+ let count = data?.initialCount || 0;
+ const display = parentElement.querySelector("#display");
+ const incrementBtn = parentElement.querySelector("#increment");
+ const decrementBtn = parentElement.querySelector("#decrement");
+ const resetBtn = parentElement.querySelector("#reset");
+
+ const updateDisplay = () => {
+ display.textContent = count;
+ setStateValue("count", count); // Persistent state
+ };
+
+ incrementBtn.onclick = () => {
+ count++;
+ updateDisplay();
+ };
+
+ decrementBtn.onclick = () => {
+ count--;
+ updateDisplay();
+ };
+
+ resetBtn.onclick = () => {
+ count = 0;
+ updateDisplay();
+ setTriggerValue("reset", true); // One-time trigger
+ };
+
+ // Initialize
+ updateDisplay();
+}
+```
+
+```python filename="my_component/__init__.py"
+from pathlib import Path
+import streamlit as st
+
+component_dir = Path(__file__).parent
+
+
+@st.cache_data
+def load_component_code():
+ with open(component_dir / "component.css", "r") as f:
+ CSS = f.read()
+ with open(component_dir / "component.html", "r") as f:
+ HTML = f.read()
+ with open(component_dir / "component.js", "r") as f:
+ JS = f.read()
+ return HTML, CSS, JS
+
+
+HTML, CSS, JS = load_component_code()
+
+interactive_counter = st.components.v2.component(
+ name="interactive_counter",
+ html=HTML,
+ css=CSS,
+ js=JS,
+)
+```
+
+```python filename="streamlit_app.py"
+from my_component import interactive_counter
+
+result = interactive_counter(
+ default={"count": 0},
+ data={"initialCount": 0},
+ on_count_change=lambda: None,
+ on_reset_change=lambda: None,
+)
+
+st.write(f"Current count: {result.count}")
+
+if result.reset:
+ st.toast("Counter was reset!")
+```
+
+## How it works
+
+### State and trigger values
+
+You can have multiple state and trigger values for a single component, and this component uses both. The `count` state value is persistent across reruns and the `reset` trigger value is transient, returning `True` for one rerun when the reset button is clicked.
+
+The increment, decrement, and reset buttons have event listeners that update the `count` state value and report the change to Python with `setStateValue()`. Additionally, the reset button sets the `reset` trigger value to `True` with `setTriggerValue()`, which is only available for one rerun. This means that when the reset button is clicked, both the state and trigger values are updated, but this will only trigger a single rerun of the script.
+
+### Component initialization
+
+The `data.initialCount` value sets the initial value of the component on the frontend. The `default` parameter sets the initial value of the component in Python. Using optional chaining (`data?.initialCount`) handles cases where data might be undefined. In this example, the component will fallback to an initial count of `0` if no initial count is provided in `data`. Therefore, when the initial count is `0`, you can omit the `data` parameter.
+
+### The `on_count_change` and `on_reset_change` callbacks
+
+The callbacks ensure that the `count` and `reset` attributes are available in the component's result object before they are set from the frontend. This is important because the component's result object is used to access the component's state and trigger values in Python. Without the callbacks, your Python code must check for the presence of the attributes before accessing them.
+
+In this example, the callbacks are set to `lambda: None`, which is an empty callback function.
+
+### Multifile component structure
+
+When your component is complex enough to warrant multiple files, it's recommended to use package-based development, which requires defining `pyproject.toml` files and understanding the basics of packaging Python projects. For simplicity, this example uses inline development and passes the contents of the HTML, CSS, and JavaScript files to the component registration function as strings. This is because you can't use file references with inline components. After understanding the basics of creating a custom component with inline development, you can explore [package-based development](/develop/concepts/custom-components/components-v2/package-based).
diff --git a/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md b/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md
new file mode 100644
index 000000000..d7acd6fd6
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/radial-menu.md
@@ -0,0 +1,376 @@
+---
+title: "Component example: Radial Menu"
+slug: /develop/concepts/custom-components/components-v2/examples/radial-menu
+description: A radial selection menu demonstrating state values for persistent selections.
+keywords: custom components v2, example, radial menu, state values, setStateValue, dynamic elements
+---
+
+# Component example: Radial Menu
+
+This is a circular menu component that allows users to select from a list of options.
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- CSS custom properties for dynamic positioning (`--i`, `--total`)
+- Document-level event listeners
+- Complex animations with CSS transitions
+
+## File-based version
+
+```none hideHeader
+project_directory/
+├── radial_menu_component/
+│ ├── __init__.py
+│ ├── menu.css
+│ ├── menu.html
+│ └── menu.js
+└── streamlit_app.py
+```
+
+
+
+```python filename="radial_menu_component/__init__.py"
+from pathlib import Path
+import streamlit as st
+
+component_dir = Path(__file__).parent
+
+
+@st.cache_data
+def load_component_code():
+ with open(component_dir / "menu.css", "r") as f:
+ CSS = f.read()
+ with open(component_dir / "menu.html", "r") as f:
+ HTML = f.read()
+ with open(component_dir / "menu.js", "r") as f:
+ JS = f.read()
+ return HTML, CSS, JS
+
+
+HTML, CSS, JS = load_component_code()
+
+radial_menu = st.components.v2.component(
+ name="radial_menu",
+ html=HTML,
+ css=CSS,
+ js=JS,
+)
+```
+
+
+
+```markup filename="radial_menu_component/menu.html"
+
+
+
+
+
+
+
+
+
+```
+
+
+
+```css filename="radial_menu_component/menu.css"
+.radial-menu {
+ position: relative;
+ display: inline-block;
+ font-family: var(--st-font);
+}
+
+/* The circular selector button and menu items*/
+.menu-selector,
+.menu-item {
+ width: 3.25rem;
+ height: 3.25rem;
+ border-radius: 50%;
+ border: 2px solid var(--st-border-color);
+ cursor: pointer;
+ background: var(--st-secondary-background-color);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+ font-size: 1.5rem;
+}
+
+.menu-selector:hover {
+ transform: scale(1.05);
+ border-color: var(--st-primary-color);
+}
+
+/* Overlay container */
+.menu-overlay {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 100;
+ pointer-events: none;
+}
+
+/* The ring of menu items */
+.menu-ring {
+ position: relative;
+ width: 13rem;
+ height: 13rem;
+ transform: scale(0);
+ opacity: 0;
+ transition:
+ transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1),
+ opacity 0.2s ease;
+}
+
+.menu-ring.open {
+ transform: scale(1);
+ opacity: 1;
+ pointer-events: auto;
+}
+
+/* Menu items arranged in a circle */
+.menu-item {
+ --angle: calc(var(--i) * (360deg / var(--total, 6)) - 90deg);
+ --radius: 4rem;
+
+ background: var(--st-background-color);
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ margin: -1.6125rem;
+ transform: rotate(var(--angle)) translate(var(--radius))
+ rotate(calc(-1 * var(--angle)));
+}
+
+.menu-item:hover {
+ transform: rotate(var(--angle)) translate(var(--radius))
+ rotate(calc(-1 * var(--angle))) scale(1.15);
+ border-color: var(--st-primary-color);
+ background: var(--st-secondary-background-color);
+}
+
+.menu-item.selected {
+ border-color: var(--st-primary-color);
+ background: var(--st-secondary-background-color);
+}
+
+/* Backdrop when menu is open */
+.menu-overlay::before {
+ content: "";
+ position: fixed;
+ inset: -100vh -100vw;
+ background: var(--st-background-color);
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ pointer-events: none;
+ z-index: -1;
+}
+
+.menu-overlay.open::before {
+ opacity: 0.7;
+ pointer-events: auto;
+}
+
+/* Center decoration */
+.menu-ring::after {
+ content: "";
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ width: 2rem;
+ height: 2rem;
+ transform: translate(-50%, -50%);
+ border-radius: 50%;
+ background: var(--st-secondary-background-color);
+ border: 2px dashed var(--st-border-color);
+ opacity: 0.6;
+ box-sizing: border-box;
+}
+```
+
+
+
+
+
+```javascript filename="radial_menu_component/menu.js"
+export default function ({ parentElement, data, setStateValue }) {
+ const selector = parentElement.querySelector("#selector");
+ const selectorIcon = parentElement.querySelector("#selector-icon");
+ const overlay = parentElement.querySelector("#overlay");
+ const ring = parentElement.querySelector("#ring");
+
+ let isOpen = false;
+ const options = data?.options || {};
+ let currentSelection = data?.selection || Object.keys(options)[0];
+
+ // Create menu items from options
+ Object.entries(options).forEach(([value, icon], index) => {
+ const button = document.createElement("button");
+ button.className = "menu-item";
+ button.dataset.value = value;
+ button.style.setProperty("--i", index);
+ button.style.setProperty("--total", Object.keys(options).length);
+ button.textContent = icon;
+
+ button.addEventListener("click", () => {
+ currentSelection = value;
+ updateDisplay();
+ toggleMenu();
+ setStateValue("selection", currentSelection);
+ });
+
+ ring.appendChild(button);
+ });
+
+ // Update the selector icon and highlight selected item
+ function updateDisplay() {
+ selectorIcon.textContent = options[currentSelection] || "?";
+
+ ring.querySelectorAll(".menu-item").forEach((item) => {
+ item.classList.toggle(
+ "selected",
+ item.dataset.value === currentSelection,
+ );
+ });
+ }
+
+ // Toggle menu open/closed
+ function toggleMenu() {
+ isOpen = !isOpen;
+ overlay.classList.toggle("open", isOpen);
+ ring.classList.toggle("open", isOpen);
+ }
+
+ // Initialize display
+ updateDisplay();
+
+ // Selector click toggles menu
+ selector.addEventListener("click", toggleMenu);
+
+ // Click outside closes menu
+ overlay.addEventListener("click", (e) => {
+ if (e.target === overlay) toggleMenu();
+ });
+
+ // Document-level click to close
+ const handleOutsideClick = (e) => {
+ if (isOpen && !parentElement.contains(e.target)) {
+ toggleMenu();
+ }
+ };
+ document.addEventListener("click", handleOutsideClick);
+
+ // Cleanup
+ return () => {
+ document.removeEventListener("click", handleOutsideClick);
+ };
+}
+```
+
+
+
+```python filename="streamlit_app.py"
+import streamlit as st
+from radial_menu_component import radial_menu
+
+st.header("Radial Menu Component")
+
+st.write("Click the button to open the menu. Select your favorite food!")
+
+options = {
+ "pizza": "🍕",
+ "burger": "🍔",
+ "taco": "🌮",
+ "ramen": "🍜",
+ "sushi": "🍣",
+ "salad": "🥗",
+}
+
+result = radial_menu(
+ data={"options": options, "selection": "burger"},
+ default={"selection": "burger"},
+ on_selection_change=lambda: None,
+ key="food_menu",
+)
+
+if result.selection:
+ icon = options.get(result.selection, "")
+ st.write(f"You selected: **{icon} {result.selection.title()}**")
+```
+
+## How it works
+
+### State values for selection
+
+When the user selects an item, `setStateValue("selection", currentSelection)` updates the persistent state. Unlike trigger values, this persists across reruns:
+
+```javascript
+button.addEventListener("click", () => {
+ currentSelection = value;
+ updateDisplay();
+ toggleMenu();
+ setStateValue("selection", currentSelection);
+});
+```
+
+### Default values
+
+The `default` parameter ensures the component has a consistent initial state without triggering an unnecessary rerun:
+
+```python
+result = radial_menu(
+ data={"options": options, "selection": "burger"},
+ default={"selection": "burger"}, # Matches initial selection
+ on_selection_change=lambda: None,
+)
+```
+
+### Dynamic element generation
+
+Menu items are created dynamically based on the options passed via `data`:
+
+```javascript
+Object.entries(options).forEach(([value, icon], index) => {
+ const button = document.createElement("button");
+ button.style.setProperty("--i", index);
+ button.style.setProperty("--total", Object.keys(options).length);
+ // ...
+ ring.appendChild(button);
+});
+```
+
+### CSS-based circular positioning
+
+The CSS uses custom properties to calculate each item's angle:
+
+```css
+.menu-item {
+ --angle: calc(var(--i) * (360deg / var(--total, 6)) - 90deg);
+ transform: rotate(var(--angle)) translate(var(--radius))
+ rotate(calc(-1 * var(--angle)));
+}
+```
+
+### Cleanup for document listeners
+
+Since the component adds a document-level click handler, it must be removed in the cleanup function:
+
+```javascript
+return () => {
+ document.removeEventListener("click", handleOutsideClick);
+};
+```
+
+## Related documentation
+
+- [Component registration](/develop/concepts/custom-components/components-v2/register)
+- [Component mounting](/develop/concepts/custom-components/components-v2/mount)
+- [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate)
+- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers)
diff --git a/content/develop/concepts/custom-components/components-v2/examples/rich-data.md b/content/develop/concepts/custom-components/components-v2/examples/rich-data.md
new file mode 100644
index 000000000..75e4e1a25
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/rich-data.md
@@ -0,0 +1,109 @@
+---
+title: "Component example: Rich data"
+slug: /develop/concepts/custom-components/components-v2/examples/rich-data
+description: A component that receives different data types from Python including DataFrames, JSON, and base64 images.
+keywords: custom components v2, example, data exchange, Arrow, DataFrame, JSON, base64
+---
+
+# Component example: Rich data
+
+This is a component that receives various data types from Python, including an Arrow-serializable dataframe, a JSON-serializable dictionary, and a Base64-encoded image.
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- Passing data from Python via the `data` parameter and accessing it in JavaScript
+- Automatic dataframe and JSON serialization
+- Passing an image as a a Base64-encoded string
+- Using a placeholder in the component's HTML and dynamically updating it with received data
+
+## Complete code
+
+```python filename="streamlit_app.py"
+import pandas as pd
+import streamlit as st
+import base64
+
+# Create sample data
+@st.cache_data
+def create_sample_df():
+ return pd.DataFrame(
+ {
+ "name": ["Alice", "Bob", "Charlie"],
+ "city": ["New York", "London", "Tokyo"],
+ }
+ )
+
+df = create_sample_df()
+
+# Load an image and convert to b64 string
+@st.cache_data
+def load_image_as_base64(image_path):
+ with open(image_path, "rb") as img_file:
+ img_bytes = img_file.read()
+ return base64.b64encode(img_bytes).decode("utf-8")
+
+img_base64 = load_image_as_base64("favi.png")
+
+# Serialization is automatically handled by Streamlit components
+data_component = st.components.v2.component(
+ "data_display",
+ html="""
+
+ `;
+ }
+ """,
+)
+
+data_component(
+ data={
+ "df": df, # Arrow-serializable dataframe
+ "user_info": {"name": "Alice"}, # JSON-serializable data
+ "image_base64": img_base64, # Image as base64 string
+ }
+)
+```
+
+## How it works
+
+### The `data` parameter
+
+When mounting a component, the `data` parameter passes information from Python to JavaScript. Streamlit automatically serializes the data:
+
+- DataFrames are converted to Apache Arrow format if passed directly to `data` or included as a value in a dictionary.
+- Dictionaries, lists, strings, numbers, booleans are JSON-serialized.
+- Bytes can be passed directly to `data`, but can't be passed as a value in a dictionary.
+
+### Accessing data in JavaScript
+
+The `data` property is available in the component function's argument object:
+
+```javascript
+export default function ({ data, parentElement }) {
+ // data contains everything passed from Python
+ const df = data.df;
+ const userInfo = data.user_info;
+}
+```
+
+
+
+DataFrames arrive as Arrow-formatted data on the frontend. In this simple example, they're converted to a string for display. For more sophisticated handling, you can use libraries like Apache Arrow JS to parse and manipulate the data.
+
+
+
+### Dynamic updates
+
+When `data` changes between reruns, your component's JavaScript function is called again with the new data. This enables reactive components that update based on Python state. For an example of a bidirectional reactive component, see the [Text input component example](/develop/concepts/custom-components/components-v2/examples/text-input).
diff --git a/content/develop/concepts/custom-components/components-v2/examples/simple-button.md b/content/develop/concepts/custom-components/components-v2/examples/simple-button.md
new file mode 100644
index 000000000..eaaa833ea
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/simple-button.md
@@ -0,0 +1,78 @@
+---
+title: "Component example: Simple button"
+slug: /develop/concepts/custom-components/components-v2/examples/simple-button
+description: An interactive button component that sends trigger values to Python when clicked.
+keywords: custom components v2, example, button, trigger values, setTriggerValue, callbacks
+---
+
+# Component example: Simple button
+
+This is an interactive button that sends events to Python. It demonstrates basic frontend-to-backend communication.
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- Component registration with HTML, CSS, and JavaScript
+- One-time trigger values sent from JavaScript with `setTriggerValue()`
+- Callback functions using the `on__change` naming pattern
+- Accessing trigger values from the component's return object
+
+## Complete code
+
+```python filename="streamlit_app.py"
+import streamlit as st
+
+if "click_count" not in st.session_state:
+ st.session_state.click_count = 0
+
+
+def handle_button_click():
+ st.session_state.click_count += 1
+
+
+my_component = st.components.v2.component(
+ "simple_button",
+ html="""""",
+ css="""
+ button {
+ border: none;
+ padding: .5rem;
+ border-radius: var(--st-button-radius);
+ background-color: var(--st-primary-color);
+ color: white;
+ }
+ """,
+ js="""
+ export default function(component) {
+ const { setTriggerValue, parentElement } = component;
+ parentElement.querySelector("button").onclick = () => {
+ setTriggerValue("action", "button_clicked");
+ };
+ }
+ """,
+)
+
+result = my_component(on_action_change=handle_button_click)
+
+if result.action:
+ st.write(f"Button clicked! Total clicks: {st.session_state.click_count}")
+```
+
+## How it works
+
+### Trigger values
+
+When the button is clicked, `setTriggerValue("action", "button_clicked")` sends a one-time event to Python. The makes the component's `"action"` attribute return the string `"button_clicked"` in Python.
+
+### Callback registration
+
+The `on_action_change` parameter registers a callback that runs when the trigger fires. The callback name follows the pattern `on__change`. Always register a callback for trigger values, even an empty one like `lambda: None`. This ensures the result object always has an attribute for the trigger. In this example, if a callback isn't defined, `"action"` won't exist as a result attribute until the trigger fires. This can cause an `AttributeError` if you try to access it before the trigger fires.
+
+In this example, the callback is used to increment the value of `st.session_state.click_count`. This is a simple way to track the number of times the button has been clicked. Alternatively, you could use a state value in your component to track the number of clicks. For an example of a component that uses a state value to track the number of clicks, see the [interactive counter](/develop/concepts/custom-components/components-v2/examples/interactive-counter) example.
+
+### Accessing the trigger
+
+The trigger value is accessible via `result.action`. Trigger values are transient, just like `st.button()` values. However, you can configure triggers to return different data types and values for different events. In this example, the `"action"` attribute returns the string `"button_clicked"` when the button is clicked and `None` otherwise. You can create complex components that return different values to indicate different user actions.
diff --git a/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md b/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md
new file mode 100644
index 000000000..215854145
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/simple-checkbox.md
@@ -0,0 +1,118 @@
+---
+title: "Component example: Simple checkbox"
+slug: /develop/concepts/custom-components/components-v2/examples/simple-checkbox
+description: A simple checkbox component that sends persistent state values to Python.
+keywords: custom components v2, example, checkbox, state values, setStateValue, callbacks
+---
+
+# Component example: Simple checkbox
+
+This is a simple checkbox that sends a state value to Python when toggled, demonstrating persistent state communication.
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- Persistent state values sent from JavaScript with `setStateValue()`
+- Callback functions with the `on__change` naming pattern
+- Initializing a stateful component with the `data` and `default` parameters
+- Using font from the app's theme
+- Accessing state values from the component's return object
+
+## Complete code
+
+```python filename="streamlit_app.py"
+import streamlit as st
+
+
+def handle_checkbox_change():
+ st.toast(f"Checkbox is now: {st.session_state.my_checkbox.checked}")
+
+
+my_component = st.components.v2.component(
+ "simple_checkbox",
+ html="""
+
+ """,
+ css="""
+ .checkbox-container {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ font-family: var(--st-font);
+ color: var(--st-text-color);
+ cursor: pointer;
+ }
+ input[type="checkbox"] {
+ width: 1.25rem;
+ height: 1.25rem;
+ accent-color: var(--st-primary-color);
+ cursor: pointer;
+ }
+ """,
+ js="""
+ export default function({ parentElement, data, setStateValue }) {
+ const checkbox = parentElement.querySelector("#checkbox");
+
+ // Initialize from data
+ checkbox.checked = data?.checked ?? false;
+
+ // Send state on change
+ checkbox.addEventListener("change", () => {
+ setStateValue("checked", checkbox.checked);
+ });
+ }
+ """,
+)
+
+initial_state = False
+
+result = my_component(
+ data={"checked": initial_state},
+ default={"checked": initial_state},
+ on_checked_change=handle_checkbox_change,
+ key="my_checkbox",
+)
+
+st.write(f"Current state: {'Enabled' if result.checked else 'Disabled'}")
+```
+
+## How it works
+
+### State values vs trigger values
+
+This example uses `setStateValue()` instead of `setTriggerValue()`. State and trigger values have the following key differences:
+
+- State values persist across reruns. Use state values for data that represents the current state of your component.
+- Trigger values reset after each rerun. Use trigger values for one-time events like button clicks.
+
+### The `default` parameter
+
+The `default` parameter sets the initial value in Python without triggering a rerun:
+
+```python
+result = my_component(
+ data={"checked": initial_state},
+ default={"checked": initial_state}, # Matches data
+ on_checked_change=handle_checkbox_change,
+)
+```
+
+To set a default value for a state value, you must also have an accompanying callback function, even if it's an empty one like `lambda: None`. The `on_checked_change` callback ensures that the `"checked"` attribute is available for the component and the `default` parameter sets its initial value in Python.
+
+If you set a default value without an associated callback function, the mounting command will raise an error because it won't recognize the state name. Conversely, if you declare a callback function without a default value, the state will be `None` until the component calls `setStateValue()` from JavaScript.
+
+### The `data` parameter
+
+The JavaScript code reads `data.checked` to initialize the checkbox state on the frontend:
+
+```javascript
+checkbox.checked = data?.checked ?? false;
+```
+
+This is a common pattern for initializing component state: `default` initializes the state in Python and `data` is used to intialize the state on the frontend. Some components might not have more than one initial state, in which case you can use the `default` parameter alone.
diff --git a/content/develop/concepts/custom-components/components-v2/examples/text-input.md b/content/develop/concepts/custom-components/components-v2/examples/text-input.md
new file mode 100644
index 000000000..6e55c7d02
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/examples/text-input.md
@@ -0,0 +1,251 @@
+---
+title: "Component example: Text input"
+slug: /develop/concepts/custom-components/components-v2/examples/text-input
+description: A text input component demonstrating bidirectional communication with programmatic updates from Python.
+keywords: custom components v2, example, text input, bidirectional communication, session state, feedback loop
+---
+
+# Component example: Text input
+
+This is a text input component that demonstrates full bidirectional communication, including programmatic updates from Python.
+
+
+
+## Key concepts demonstrated
+
+This component demonstrates the following concepts:
+
+- Mounting a component with a key and reading component state from Session State
+- Wrapping a component's raw mounting command to create a user-friendly mounting command
+- Programmatic updates from Python via the `data` parameter
+- Syncing frontend state without interrupting user input
+
+## Complete code
+
+For easy copying and pasting, expand the complete code below. However, for easier reading, the code is split into multiple files in the next section.
+
+
+
+```python filename="streamlit_app.py"
+import streamlit as st
+
+HTML = """
+
+
+"""
+
+JS = """
+ export default function(component) {
+ const { setStateValue, parentElement, data } = component;
+
+ const label = parentElement.querySelector('label');
+ label.innerText = data.label;
+
+ const input = parentElement.querySelector('input');
+ if (input.value !== data.value) {
+ input.value = data.value ?? '';
+ };
+
+ input.onkeydown = (e) => {
+ if (e.key === 'Enter') {
+ setStateValue('value', e.target.value);
+ }
+ };
+
+ input.onblur = (e) => {
+ setStateValue('value', e.target.value);
+ };
+ }
+"""
+
+text_component = st.components.v2.component(
+ "my_text_input",
+ html=HTML,
+ js=JS,
+)
+
+
+def text_component_wrapper(
+ label, *, default="", key=None, on_change=lambda: None
+):
+ component_state = st.session_state.get(key, {})
+ value = component_state.get("value", default)
+ data = {"label": label, "value": value}
+ result = text_component(
+ data=data,
+ default={"value": value},
+ key=key,
+ on_value_change=on_change,
+ )
+ return result
+
+
+if st.button("Hello World"):
+ st.session_state["my_text_input_instance"]["value"] = "Hello World"
+if st.button("Clear text"):
+ st.session_state["my_text_input_instance"]["value"] = ""
+result = text_component_wrapper(
+ "Enter something",
+ default="I love Streamlit!",
+ key="my_text_input_instance",
+)
+
+st.write("Result:", result)
+st.write("Session state:", st.session_state)
+```
+
+
+
+## File-based version
+
+```none filename="Directory structure"
+my_app/
+├── streamlit_app.py
+└── my_component/
+ ├── __init__.py
+ ├── component.html
+ └── component.js
+```
+
+```markup filename="my_component/component.html"
+
+
+```
+
+```javascript filename="my_component/component.js"
+export default function (component) {
+ const { setStateValue, parentElement, data } = component;
+
+ const label = parentElement.querySelector("label");
+ label.innerText = data.label;
+
+ const input = parentElement.querySelector("input");
+ if (input.value !== data.value) {
+ input.value = data.value ?? "";
+ }
+
+ input.onkeydown = (e) => {
+ if (e.key === "Enter") {
+ setStateValue("value", e.target.value);
+ }
+ };
+
+ input.onblur = (e) => {
+ setStateValue("value", e.target.value);
+ };
+}
+```
+
+```python filename="my_component/__init__.py"
+from pathlib import Path
+import streamlit as st
+
+component_dir = Path(__file__).parent
+
+@st.cache_data
+def load_component_code():
+ with open(component_dir / "component.html", "r") as f:
+ HTML = f.read()
+ with open(component_dir / "component.js", "r") as f:
+ JS = f.read()
+ return HTML, JS
+
+HTML, JS = load_component_code()
+
+text_component = st.components.v2.component(
+ name="my_text_input",
+ html=HTML,
+ js=JS,
+)
+
+def text_component_wrapper(
+ label, *, default="", key=None, on_change=lambda: None
+):
+ component_state = st.session_state.get(key, {})
+ value = component_state.get("value", default)
+ data = {"label": label, "value": value}
+ result = text_component(
+ data=data,
+ default={"value": value},
+ key=key,
+ on_value_change=on_change,
+ )
+ return result
+```
+
+```python filename="streamlit_app.py"
+from my_component import text_component_wrapper
+
+if st.button("Hello World"):
+ st.session_state["my_text_input_instance"]["value"] = "Hello World"
+if st.button("Clear text"):
+ st.session_state["my_text_input_instance"]["value"] = ""
+result = text_component_wrapper(
+ "Enter something",
+ default="I love Streamlit!",
+ key="my_text_input_instance",
+)
+
+st.write("Result:", result)
+st.write("Session state:", st.session_state)
+```
+
+## How it works
+
+### The wrapper function pattern
+
+The wrapper function creates a reusable interface for your component:
+
+```python
+def my_component_wrapper(
+ label, *, default="", key=None, on_change=lambda: None
+):
+ # Read current state from Session State
+ component_state = st.session_state.get(key, {})
+ value = component_state.get("value", default)
+
+ # Pass current value to component
+ data = {"label": label, "value": value}
+ result = my_component(
+ data=data,
+ default={"value": value},
+ key=key,
+ on_value_change=on_change,
+ )
+ return result
+```
+
+This pattern:
+
+1. Reads the current value from Session State (falling back to `default`)
+2. Passes the value to the component via `data`
+3. Returns the result for the caller to use
+
+### Syncing without interruption
+
+The JavaScript checks if the value has actually changed before updating:
+
+```javascript
+if (input.value !== data.value) {
+ input.value = data.value ?? "";
+}
+```
+
+This prevents the input from being overwritten while the user is typing. Without this check, each rerun would reset the input to the last committed value.
+
+### Programmatic updates
+
+Buttons can modify Session State directly:
+
+```python
+if st.button("Hello World"):
+ st.session_state["my_text_input_instance"]["value"] = "Hello World"
+```
+
+On the next rerun, the wrapper reads this new value from Session State and passes it to the component via `data`. The JavaScript then updates the input field.
+
+## Related documentation
+
+- [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate)
+- [Component mounting](/develop/concepts/custom-components/components-v2/mount)
+- [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers)
diff --git a/content/develop/concepts/custom-components/components-v2/mount.md b/content/develop/concepts/custom-components/components-v2/mount.md
new file mode 100644
index 000000000..0ab7c5e2b
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/mount.md
@@ -0,0 +1,274 @@
+---
+title: Component mounting
+slug: /develop/concepts/custom-components/components-v2/mount
+description: Learn how to mount custom v2 components in your Streamlit app, pass data, handle callbacks, and access component values.
+keywords: custom components v2, component mounting, BidiComponentCallable, key, data, default, callbacks, component values
+---
+
+# Component mounting
+
+After registering your component, you must mount your component in your Streamlit app. This creates a specific instance of the component and is equivalent to calling a native Streamlit command like `st.button()` or `st.text_input()`. This is where you pass data to the component and handle its output.
+
+## Basic examples
+
+### Hello world
+
+This is the [hello world](/develop/concepts/custom-components/components-v2/examples/hello-world) component shown in the quickstart guide. It's a static component that displays "Hello, World!" using the app's theme colors.
+
+```python
+import streamlit as st
+
+hello_component = st.components.v2.component(
+ name="my_hello_world",
+ html="
Hello, World!
",
+ css="h2 { color: var(--st-primary-color); }",
+)
+
+hello_component() # Mount the component
+hello_component(key="second_instance") # Mount another instance of the component
+```
+
+### Simple button
+
+This is the [simple button](/develop/concepts/custom-components/components-v2/examples/simple-button) component shown in the quickstart guide. It's an interactive button that sends a trigger value to Python when clicked.
+
+```python
+import streamlit as st
+
+my_component = st.components.v2.component(
+ name="my_button",
+ html="",
+ css="button { background: var(--st-primary-color); color: white; }",
+ js="""
+ export default function(component) {
+ const { setTriggerValue, parentElement } = component;
+ parentElement.querySelector("button").onclick = () => {
+ setTriggerValue("action", "button_clicked");
+ };
+ }
+ """
+)
+
+result = my_component(on_action_change=lambda: None)
+```
+
+### Simple checkbox
+
+This is the [simple checkbox](/develop/concepts/custom-components/components-v2/examples/simple-checkbox) component shown in the quickstart guide. It's a simple checkbox that reports a stateful value to Python when toggled.
+
+```python
+import streamlit as st
+
+simple_component = st.components.v2.component(
+ name="simple_checkbox",
+ html="""""",
+ js="""
+ export default function({ parentElement, data, setStateValue, key }) {
+ const checkbox = parentElement.querySelector("input[type='checkbox']");
+ const enabled = data.enabled;
+
+ // Initialize checkbox state
+ checkbox.checked = enabled;
+
+ // Update state when checkbox is toggled
+ checkbox.addEventListener("change", () => {
+ setStateValue("enabled", checkbox.checked);
+ });
+ }
+ """
+)
+
+initial_state = True
+
+result = simple_component(
+ data={"enabled": initial_state},
+ default={"enabled": initial_state},
+ on_enabled_change=lambda: None
+)
+```
+
+## Mounting parameters
+
+All mounting parameters are keyword-only and optional. The available parameters are documented in the [`BidiComponentCallable`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentcallable) class.
+
+### Component identity (`key`)
+
+Components use the Python `key` parameter in the same manner as widgets. For a detailed overview of keys in widgets, see [Understanding widget behavior](/develop/concepts/architecture/widget-behavior#keys-help-distinguish-widgets-and-access-their-values).
+
+Just like widgets, components have internally computed identities that help Streamlit match component mounting commands to their frontend instances.
+
+- If you pass a key when you mount your component, Streamlit will update the existing frontend element when other parameters change.
+- If you don't pass a key when you mount your component, Streamlit will create a new frontend element when other parameters change. This will reset the component's state.
+
+Additionally, you must use keys to disambiguate between otherwise identical instances of the same component.
+
+```python
+# Multiple instances of the same component
+result1 = my_component(key="first_instance")
+result2 = my_component(key="second_instance")
+```
+
+In the hello world example, two instances of the same component are mounted. Because there is no other data passed to the instances, for Streamlit to compute unique identities, at least one of the instances must be given a key:
+
+```python
+hello_component() # Mount the component
+hello_component(key="second_instance") # Mount another instance of the component
+```
+
+If you remove the key from the second instance, you would get a `StreamlitDuplicateElementID` error. This is the component's equivalent to a `DuplicateWidgetID` error.
+
+
+
+The `key` property available in JavaScript in the `ComponentArgs` type isn't the same as the Python `key` parameter. On the frontend, the JavaScript `key` is a dynamically generated identifier that is only usable for a specific instance of the component. For example, the JavaScript `key` will change if you mount a component, navigate away from the page, and then navigate back to remount it.
+
+
+
+### State and trigger callbacks (`on__change` or `on__change`)
+
+For each state and trigger value for your component, you must pass a callback function to the component mounting command. For example, if you have a trigger named `"click"`, then you must pass a callback function to the keyword argument `on_click_change`.
+
+```python
+result = my_component(on_action_change=lambda: None)
+```
+
+In general, to create a callback's keyword argument name, prefix your state or trigger name with `on_` and then suffix it with `_change`. If you don't need to execute any Python logic in a callback, you can pass `lambda: None` as the callback function.
+
+Component callback functions work similarly to [widget callback functions](/develop/concepts/architecture/widget-behavior#order-of-operations). For components, `setStateValue()` and `setTriggerValue()` start the Python rerun process from the component's JavaScript code. However, there are two important distinctions compared to widget callback functions:
+
+- Because components can have multiple states and triggers, a single component instance can have multiple callbacks and also execute multiple callbacks in one script rerun. This is explained in more detail on the next page, [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate).
+- Component callback functions play an important role in shaping the component's result, which is accessed through the component's return value and Session State.
+
+By passing a callback function for each of your component's state and trigger values, you ensure that all of your component's state and trigger values are consistently available in the component's result object:
+
+- In the simple button example, the callback is set with `on_action_change=lambda: None`. Because the callback is defined, even trivially, the result returned by the component will always have an `action` attribute. This attribute has a value of `None` until the button is clicked.
+- In the simple checkbox example, the callback is set with `on_enabled_change=lambda: None`. Because the callback is defined, even trivially, the result returned by the component will always have an `enabled` attribute. The default value is configured with the `data` parameter and is `True`.
+
+
+
+The `None` default value for triggers isn't related to using `lambda: None` as the callback function. A trigger always has a default value of `None`, regardless of what callback function it has.
+
+
+
+### Customizing and updating an instance (`data` and `default`)
+
+In a component mounting command, there are two parameters that you can use to customize and update a component instance: `data` and `default`.
+
+The `data` parameter passes information from Python to your component's frontend. It supports JSON-serializable, Arrow-serializable, and raw bytes data. Commonly this is a single value or a dictionary of values that you retrieve in your JavaScript function.
+
+```python
+result = my_component(
+ data={
+ "df": df, # Arrow-serializable dataframe
+ "user_info": {"name": "Alice"}, # JSON-serializable data
+ "image_base64": img_base64 # Image as base64 string
+ }
+)
+```
+
+DataFrames are automatically serialized using Apache Arrow format, which provides efficient transfer and preserves data types. On the frontend, you can work with the Arrow data directly or convert it to other formats as needed.
+
+The `default` parameter sets the initial values for your component's state _in Python_. This is a dictionary where each key is a state name. Each state name has an accompanying callback function passed as a keyword argument named `on__change`. Because `default` only sets the initial value in Python, you must appropriately pass data to the component's `data` parameter to ensure that the component is consistent with its intended initial state.
+
+#### Initialize component state
+
+In general, the `default` parameter is used to avoid a rerun of the script when the component is mounted. Otherwise, your component might need to immediately call `setStateValue()` when it's mounted to inform Python of its initial state. Unnecessary reruns are inefficient and might increase the chance of visual flickering.
+
+The simple checkbox example demonstrates how to use the `default` parameter to avoid a rerun of the script when the component is mounted. An initial value of `True` is set for the `"enabled"` state:
+
+```python
+initial_state = True
+
+result = simple_component(
+ data={"enabled": initial_state},
+ default={"enabled": initial_state},
+ on_enabled_change=lambda: None
+)
+```
+
+In the component's JavaScript function, the initial DOM state is set from the `"enabled"` key in the `data` parameter:
+
+```javascript
+export default function ({ parentElement, data, setStateValue, key }) {
+ const checkbox = parentElement.querySelector("input[type='checkbox']");
+ const enabled = data.enabled;
+
+ // Initialize checkbox state
+ checkbox.checked = enabled;
+
+ // Update state when checkbox is toggled
+ checkbox.addEventListener("change", () => {
+ setStateValue("enabled", checkbox.checked);
+ });
+}
+```
+
+In the previous example, if your remove `default={"enabled": initial_state},` from the Python code, then the initial state of the `"enabled"` key would be `None`, which would be out of sync with the frontend until the first user interaction. In this case, you would have to add `setStateValue("enabled", enabled);` to the JavaScript code to ensure that the initial state is set correctly.
+
+```diff
+export default function({ parentElement, data, setStateValue, key }) {
+ const checkbox = parentElement.querySelector("input[type='checkbox']");
+ const enabled = data.enabled;
+
+ // Initialize checkbox state
+ checkbox.checked = enabled;
++ setStateValue("enabled", enabled);
+
+ // Update state when checkbox is toggled
+ checkbox.addEventListener("change", () => {
+ setStateValue("enabled", checkbox.checked);
+ });
+}
+```
+
+This causes an unnecessary rerun of the script when the component is mounted, which is why it's recommended to use the `default` parameter instead.
+
+### Layout control (`width` and `height`)
+
+To make your component compatible with the Streamlit layout system, you can pass `width` and `height` parameters to your component mounting command. These parameters match the same width and height parameters used in other Streamlit commands. Streamlit wraps your component in a `
` element and updates its `width` and `height` properties so that it behaves like other Streamlit elements.
+
+```python
+result = my_component(
+ width="stretch", # Full width
+ height=400 # Fixed height
+)
+```
+
+On the frontend, because Streamlit will size the `
` wrapper element correctly, it's generally recommended to set your component's CSS to `width: 100%; height: 100%`. If your component needs to know its exact measurements at runtime in JavaScript, you can use a `ResizeObserver` to get that information dynamically.
+
+### Theming and styling (`isolate_styles`)
+
+Custom Components v2 provides style isolation options to control whether or not to sandbox your component in a shadow root. This is useful to prevent your component's styles from leaking to the rest of the page and to prevent the page's styles from leaking into your component. By default, Streamlit uses a shadow root for your component.
+
+```python
+result = my_component(
+ isolate_styles=True # Default behavior uses a shadow root
+)
+```
+
+For more information about theming and styling, see the [Theming and styling](/develop/concepts/custom-components/components-v2/theming) guide.
+
+## Accessing component values
+
+You can access the state and trigger values of a component through the mounting command's return value. Alternatively, if you mounted your component with a key, you can access the component values through Session State.
+
+### Component return value
+
+Component mounting commands return a [`BidiComponentResult`](/develop/api-reference/custom-components/st.components.v2.types.bidicomponentresult) object that provides access to state and trigger values. You can access each state or trigger value as an attribute of the result object.
+
+```python
+result = my_component(on_action_change=lambda: None)
+st.write(result.action)
+```
+
+### Component values in Session State
+
+If you mounted your component with a key, you can access the component values through Session State. The component's result object is stored in Session State under the key you provided.
+
+```python
+result = my_component(on_action_change=lambda: None, key="my_key")
+st.write(st.session_state.my_key.action)
+```
+
+### State vs trigger behavior
+
+State and trigger values have different behavior in relation to reruns. State values persist across reruns, while trigger values are transient and reset after each rerun. For more information about state and trigger values, see the [State vs trigger values](/develop/concepts/custom-components/components-v2/state-and-triggers) guide.
diff --git a/content/develop/concepts/custom-components/components-v2/package-based.md b/content/develop/concepts/custom-components/components-v2/package-based.md
new file mode 100644
index 000000000..fc774bfaa
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/package-based.md
@@ -0,0 +1,744 @@
+---
+title: Package-based components
+slug: /develop/concepts/custom-components/components-v2/package-based
+description: Learn how to build complex Custom Components v2 using package-based development with TypeScript, modern build tools, and external dependencies.
+keywords: custom components v2, package-based components, TypeScript, build tools, Vite, Webpack, pyproject.toml, npm packages, component distribution
+---
+
+# Package-based components
+
+While inline components are perfect for rapid prototyping, package-based components provide the full power of modern frontend development. This approach is ideal for complex components that require TypeScript, external dependencies, build optimization, or distribution as Python packages.
+
+## When to use package-based components
+
+Choose package-based development when you need:
+
+- TypeScript support - Type safety and better developer experience.
+- External dependencies - React, D3, Chart.js, or other npm packages.
+- Build optimization - Code splitting, minification, and bundling.
+- Team development - Proper tooling, testing, and collaboration workflows.
+- Distribution - Publishing components as Python packages on PyPI.
+- Complex logic - Multi-file projects with organized code structure.
+
+## Project structure
+
+A typical package-based component follows this structure:
+
+```
+my-component-package/
+├── pyproject.toml # Top-level package configuration
+└── src/
+ └── my_component/
+ ├── __init__.py # Python package entry point
+ ├── component.py # Component Python API
+ ├── pyproject.toml # Component-specific configuration
+ └── frontend/
+ ├── dist/ # Built frontend assets
+ │ ├── bundle-.js
+ │ └── styles-.css
+ ├── src/ # Frontend source code
+ │ ├── index.ts # Main TypeScript entry
+ │ └── components/
+ ├── package.json # Frontend dependencies
+ ├── tsconfig.json # TypeScript configuration
+ └── vite.config.js # Build tool configuration
+```
+
+## Configuration setup
+
+### Top-level `pyproject.toml`
+
+Configure your Python package distribution. This file is located at the root of your project and is used to configure the package distribution. For more information about the `pyproject.toml` for packaging projects, see the [Python Packaging User Guide](https://packaging.python.org/en/latest/tutorials/packaging-projects/).
+
+#### Explicit package configuration (recommended)
+
+This approach explicitly lists packages and their locations. You need to identify each component module and the necessary assets to serve (frontend components and inner `pyproject.toml` file).
+
+```toml
+[project]
+name = "my_streamlit_component_package"
+version = "0.1.0"
+requires-python = ">=3.10"
+dependencies = ["streamlit>=1.51.0"]
+
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+# Explicitly list packages and their source directory
+[tool.setuptools]
+packages = ["my_component"] # List each package by name
+package-dir = {"" = "src"} # Look for packages in src/ directory
+include-package-data = true # Include non-Python files
+
+# Specify which files to include in the package
+[tool.setuptools.package-data]
+my_component = ["frontend/dist/**/*", "pyproject.toml"]
+```
+
+#### Alternative: Automatic package discovery
+
+For projects with multiple packages or complex structures, you can use automatic discovery:
+
+```toml
+[project]
+name = "my_streamlit_component_package"
+version = "0.1.0"
+requires-python = ">=3.10"
+dependencies = ["streamlit>=1.51.0"]
+
+[build-system]
+requires = ["setuptools>=61.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+# Automatically find packages matching a pattern
+[tool.setuptools.packages.find]
+where = ["src"] # Look in src/ directory
+include = ["my_component*"] # Include packages starting with "my_component"
+
+[tool.setuptools]
+include-package-data = true
+
+[tool.setuptools.package-data]
+my_component = ["frontend/dist/**/*", "pyproject.toml"]
+```
+
+### Component-level `pyproject.toml`
+
+Within your component module, you need to register your component and specify the asset directory (`asset_dir`) in the `[tool.streamlit.component.components]` table. The `asset_dir` path is relative to the component's `pyproject.toml` file. All files and subdirectories within this directory will be served by Streamlit.
+
+When you start a Streamlit app, Streamlit scans all installed packages for any Streamlit components. For each installed component, Streamlit serves the contents of its asset directory. This makes it possible to refer to images and other assets within your component's HTML and CSS code. `project.name` should match the name of your package when installed.
+
+```toml
+[project]
+name = "my_streamlit_component_package"
+version = "0.1.0"
+
+# Register your components and the asset directory.
+[[tool.streamlit.component.components]]
+name = "my_component"
+asset_dir = "frontend/dist"
+```
+
+
+
+The `asset_dir` path is relative to the component's `pyproject.toml` file. All files and subdirectories within this directory will be served publicly by Streamlit and won't be protected by any logical restrictions in your app. Don't include sensitive information in your component's asset directory.
+
+
+
+## Frontend development setup
+
+### `package.json` configuration
+
+Set up your frontend dependencies and build scripts:
+
+```json
+{
+ "name": "my-component-frontend",
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview",
+ "type-check": "tsc --noEmit"
+ },
+ "dependencies": {
+ "@streamlit/component-v2-lib": "^0.1.0"
+ },
+ "devDependencies": {
+ "@types/node": "^20.0.0",
+ "typescript": "^5.0.0",
+ "vite": "^5.0.0"
+ }
+}
+```
+
+### TypeScript configuration
+
+Configure TypeScript for optimal development:
+
+```json
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist"]
+}
+```
+
+### Vite build configuration
+
+Configure Vite for optimized builds with hashed filenames:
+
+```javascript
+// vite.config.js
+import { defineConfig } from "vite";
+
+export default defineConfig({
+ build: {
+ outDir: "dist",
+ lib: {
+ entry: "src/index.ts",
+ name: "MyComponent",
+ fileName: (format) =>
+ `bundle-[hash].${format === "es" ? "js" : "umd.js"}`,
+ formats: ["es"],
+ },
+ rollupOptions: {
+ output: {
+ assetFileNames: (assetInfo) => {
+ if (assetInfo.name?.endsWith(".css")) {
+ return "styles-[hash].css";
+ }
+ return "[name]-[hash].[ext]";
+ },
+ },
+ },
+ sourcemap: true,
+ },
+});
+```
+
+## TypeScript component development
+
+### Basic TypeScript component
+
+Create a type-safe component using the official TypeScript library:
+
+```typescript
+// src/index.ts
+import { Component, ComponentState } from "@streamlit/component-v2-lib";
+
+/** The state/trigger values this component maintains */
+interface MyComponentState extends ComponentState {
+ count: number;
+ lastAction: string;
+}
+
+/** The shape of the data passed from Python */
+interface MyComponentData {
+ initialCount: number;
+ label: string;
+ theme: "light" | "dark";
+}
+
+const MyComponent: Component = (
+ component,
+) => {
+ const { data, setStateValue, setTriggerValue, parentElement } = component;
+
+ let count = data.initialCount || 0;
+
+ // Create UI elements
+ const container = document.createElement("div");
+ container.className = "component-container";
+
+ const display = document.createElement("div");
+ display.className = "count-display";
+ display.textContent = `Count: ${count}`;
+
+ const incrementBtn = document.createElement("button");
+ incrementBtn.textContent = `${data.label || "Increment"}`;
+ incrementBtn.className = "increment-btn";
+
+ const resetBtn = document.createElement("button");
+ resetBtn.textContent = "Reset";
+ resetBtn.className = "reset-btn";
+
+ // Assemble UI
+ container.appendChild(display);
+ container.appendChild(incrementBtn);
+ container.appendChild(resetBtn);
+ parentElement.appendChild(container);
+
+ // Apply theme
+ container.setAttribute("data-theme", data.theme || "light");
+
+ // Event handlers with type safety
+ const handleIncrement = (): void => {
+ count++;
+ display.textContent = `Count: ${count}`;
+ setStateValue("count", count);
+ setTriggerValue("lastAction", "increment");
+ };
+
+ const handleReset = (): void => {
+ count = 0;
+ display.textContent = `Count: ${count}`;
+ setStateValue("count", count);
+ setTriggerValue("lastAction", "reset");
+ };
+
+ // Attach event listeners
+ incrementBtn.addEventListener("click", handleIncrement);
+ resetBtn.addEventListener("click", handleReset);
+
+ // Initialize state
+ setStateValue("count", count);
+
+ // Return cleanup function
+ return () => {
+ incrementBtn.removeEventListener("click", handleIncrement);
+ resetBtn.removeEventListener("click", handleReset);
+ };
+};
+
+export default MyComponent;
+```
+
+### Advanced component with external dependencies
+
+Here's an example using Chart.js for data visualization:
+
+```typescript
+// src/chart-component.ts
+import { Component, ComponentState } from "@streamlit/component-v2-lib";
+import { Chart, ChartConfiguration, registerables } from "chart.js";
+
+// Register Chart.js components
+Chart.register(...registerables);
+
+interface ChartComponentState extends ComponentState {
+ selectedDataPoint: number | null;
+}
+
+interface ChartData {
+ labels: string[];
+ datasets: Array<{
+ label: string;
+ data: number[];
+ backgroundColor?: string;
+ borderColor?: string;
+ }>;
+}
+
+interface ChartComponentData {
+ chartData: ChartData;
+ chartType: "line" | "bar" | "pie";
+ title?: string;
+}
+
+const ChartComponent: Component = (
+ component,
+) => {
+ const { data, setStateValue, setTriggerValue, parentElement } = component;
+
+ // Create canvas element
+ const canvas = document.createElement("canvas");
+ canvas.width = 400;
+ canvas.height = 300;
+ parentElement.appendChild(canvas);
+
+ // Chart configuration
+ const config: ChartConfiguration = {
+ type: data.chartType || "line",
+ data: data.chartData,
+ options: {
+ responsive: true,
+ plugins: {
+ title: {
+ display: !!data.title,
+ text: data.title,
+ },
+ legend: {
+ position: "top",
+ },
+ },
+ onClick: (event, elements) => {
+ if (elements.length > 0) {
+ const dataIndex = elements[0].index;
+ setStateValue("selectedDataPoint", dataIndex);
+ setTriggerValue("dataPointClicked", {
+ index: dataIndex,
+ label: data.chartData.labels[dataIndex],
+ value: data.chartData.datasets[0].data[dataIndex],
+ });
+ }
+ },
+ },
+ };
+
+ // Create chart instance
+ const chart = new Chart(canvas, config);
+
+ // Cleanup function
+ return () => {
+ chart.destroy();
+ };
+};
+
+export default ChartComponent;
+```
+
+## Python component API
+
+### Component definition
+
+Create a clean Python API for your component:
+
+```python
+# src/my_component/component.py
+import streamlit as st
+from typing import Dict, Any, Optional, Callable, Union, List
+
+def advanced_counter(
+ initial_value: int = 0,
+ label: str = "Increment",
+ theme: str = "light",
+ key: Optional[str] = None,
+ on_count_change: Optional[Callable] = None,
+ on_lastAction_change: Optional[Callable] = None
+):
+ """
+ Create an advanced counter component with TypeScript frontend.
+
+ Parameters
+ ----------
+ initial_value : int
+ The starting count value (default: 0)
+ label : str
+ The text to display on the increment button (default: "Increment")
+ theme : str
+ The component theme, either "light" or "dark" (default: "light")
+ key : str, optional
+ A unique key for the component instance
+ on_count_change : callable, optional
+ Callback function called when count changes
+ on_lastAction_change : callable, optional
+ Callback function called when an action is triggered
+
+ Returns
+ -------
+ ComponentResult
+ Object with count and lastAction properties
+ """
+
+ # Create the component using glob pattern for hashed builds
+ component = st.components.v2.component(
+ name="advanced_counter",
+ js="bundle-*.js", # Glob pattern matches hashed filename
+ css="styles-*.css", # Glob pattern matches hashed CSS
+ data={
+ "initialCount": initial_value,
+ "label": label,
+ "theme": theme
+ }
+ )
+
+ # Mount the component
+ result = component(
+ key=key,
+ default={"count": initial_value, "lastAction": None},
+ on_count_change=on_count_change,
+ on_lastAction_change=on_lastAction_change
+ )
+
+ return result
+
+def chart_component(
+ chart_data: Dict[str, Any],
+ chart_type: str = "line",
+ title: Optional[str] = None,
+ key: Optional[str] = None,
+ on_selectedDataPoint_change: Optional[Callable] = None,
+ on_dataPointClicked_change: Optional[Callable] = None
+):
+ """
+ Create an interactive chart component using Chart.js.
+
+ Parameters
+ ----------
+ chart_data : dict
+ Chart data in Chart.js format with labels and datasets
+ chart_type : str
+ Type of chart: "line", "bar", or "pie" (default: "line")
+ title : str, optional
+ Chart title to display
+ key : str, optional
+ A unique key for the component instance
+ on_selectedDataPoint_change : callable, optional
+ Callback when a data point is selected
+ on_dataPointClicked_change : callable, optional
+ Callback when a data point is clicked
+
+ Returns
+ -------
+ ComponentResult
+ Object with selectedDataPoint and dataPointClicked properties
+ """
+
+ component = st.components.v2.component(
+ name="chart_component",
+ js="chart-bundle-*.js",
+ css="chart-styles-*.css",
+ data={
+ "chartData": chart_data,
+ "chartType": chart_type,
+ "title": title
+ }
+ )
+
+ result = component(
+ key=key,
+ default={"selectedDataPoint": None},
+ on_selectedDataPoint_change=on_selectedDataPoint_change,
+ on_dataPointClicked_change=on_dataPointClicked_change
+ )
+
+ return result
+```
+
+### Package entry point
+
+Create a clean package interface:
+
+```python
+# src/my_component/__init__.py
+"""
+My Streamlit Component Package
+
+A collection of advanced custom components built with TypeScript and modern tooling.
+"""
+
+from .component import advanced_counter, chart_component
+
+__version__ = "0.1.0"
+__all__ = ["advanced_counter", "chart_component"]
+```
+
+## Glob pattern support
+
+Package-based components support glob patterns for referencing build outputs with hashed filenames:
+
+### Why use glob patterns?
+
+Modern build tools like Vite and Webpack generate hashed filenames for cache busting:
+
+```
+frontend/dist/
+├── bundle-a1b2c3d4.js # Hashed JavaScript bundle
+├── styles-e5f6g7h8.css # Hashed CSS file
+└── assets/
+ └── logo-i9j0k1l2.png # Hashed assets
+```
+
+### Glob resolution rules
+
+1. Pattern matching: `bundle-*.js` matches `bundle-a1b2c3d4.js`
+2. Single file requirement: Pattern must resolve to exactly one file
+3. Security: Matched files must be within the `asset_dir`
+4. Relative paths: Patterns are resolved relative to `asset_dir`
+
+### Example usage
+
+```python
+# These glob patterns work with hashed build outputs
+component = st.components.v2.component(
+ name="my_component",
+ js="bundle-*.js", # Matches bundle-.js
+ css="styles-*.css", # Matches styles-.css
+ data={"message": "Hello"}
+)
+```
+
+
+
+**Error Handling**: If a glob pattern matches zero files or multiple files, Streamlit will raise a clear error message to help you debug the issue.
+
+
+
+## Development workflow
+
+### Development mode
+
+During development, use Vite's dev server for hot reloading:
+
+```bash
+# Terminal 1: Start frontend dev server
+cd src/my_component/frontend
+npm run dev
+
+# Terminal 2: Run Streamlit app
+streamlit run app.py
+```
+
+For development, temporarily use the dev server URL:
+
+```python
+# Development mode (temporary)
+component = st.components.v2.component(
+ name="my_component",
+ js="http://localhost:5173/src/index.ts", # Dev server URL
+ data={"message": "Hello"}
+)
+```
+
+### Build for production
+
+Build optimized assets for production:
+
+```bash
+cd src/my_component/frontend
+npm run build
+```
+
+This generates hashed files in the `dist/` directory that your glob patterns will match.
+
+### Testing the package
+
+Test your component locally before publishing:
+
+```python
+# app.py - Test your component
+import streamlit as st
+from my_component import advanced_counter, chart_component
+
+st.title("Component Testing")
+
+# Test the counter
+counter_result = advanced_counter(
+ initial_value=5,
+ label="Click me!",
+ theme="dark",
+ key="test_counter"
+)
+
+st.write(f"Count: {counter_result.count}")
+if counter_result.lastAction:
+ st.write(f"Last action: {counter_result.lastAction}")
+
+# Test the chart
+chart_data = {
+ "labels": ["Jan", "Feb", "Mar", "Apr", "May"],
+ "datasets": [{
+ "label": "Sales",
+ "data": [12, 19, 3, 5, 2],
+ "backgroundColor": "rgba(54, 162, 235, 0.2)",
+ "borderColor": "rgba(54, 162, 235, 1)"
+ }]
+}
+
+chart_result = chart_component(
+ chart_data=chart_data,
+ chart_type="bar",
+ title="Monthly Sales",
+ key="test_chart"
+)
+
+if chart_result.selectedDataPoint is not None:
+ st.write(f"Selected data point: {chart_result.selectedDataPoint}")
+```
+
+## Publishing your package
+
+### Build the distribution
+
+```bash
+# Install build tools
+pip install build twine
+
+# Build the package
+python -m build
+```
+
+### Upload to PyPI
+
+```bash
+# Upload to Test PyPI first
+python -m twine upload --repository testpypi dist/*
+
+# After testing, upload to PyPI
+python -m twine upload dist/*
+```
+
+### Installation and usage
+
+Users can then install and use your component:
+
+```bash
+pip install my-streamlit-component-package
+```
+
+```python
+import streamlit as st
+from my_streamlit_component_package import advanced_counter
+
+result = advanced_counter(
+ initial_value=10,
+ label="Increment Counter",
+ theme="dark"
+)
+
+st.write(f"Current count: {result.count}")
+```
+
+## Best practices
+
+### Type safety
+
+Always use TypeScript interfaces for better development experience:
+
+```typescript
+interface ComponentProps {
+ data: MyComponentData;
+ setStateValue: (key: string, value: any) => void;
+ setTriggerValue: (key: string, value: any) => void;
+ parentElement: HTMLElement;
+}
+```
+
+### Error handling
+
+Implement robust error handling in both TypeScript and Python:
+
+```typescript
+// TypeScript error handling
+export default function (component) {
+ try {
+ // Component logic here
+ return () => {
+ // Cleanup logic
+ };
+ } catch (error) {
+ console.error("Component error:", error);
+ component.parentElement.innerHTML = `
Component failed to load
`;
+ }
+}
+```
+
+### Performance optimization
+
+- Use code splitting for large dependencies
+- Implement lazy loading for heavy components
+- Optimize bundle sizes with tree shaking
+
+### Documentation
+
+Provide comprehensive documentation:
+
+- TypeScript interfaces for all data shapes
+- Python docstrings with parameter descriptions
+- Usage examples and tutorials
+- Migration guides for updates
+
+## What's next?
+
+Now that you understand package-based components:
+
+- Learn about [State vs triggers](/develop/concepts/custom-components/components-v2/state-and-triggers) for interactive functionality.
+- Explore [Theming and styling](/develop/concepts/custom-components/components-v2/theming) for beautiful components.
+- Check out [Publishing components](/develop/concepts/custom-components/publish) for distribution strategies.
diff --git a/content/develop/concepts/custom-components/components-v2/register.md b/content/develop/concepts/custom-components/components-v2/register.md
new file mode 100644
index 000000000..105eeabb0
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/register.md
@@ -0,0 +1,172 @@
+---
+title: Component registration
+slug: /develop/concepts/custom-components/components-v2/register
+description: Learn how to register custom v2 components with HTML, CSS, and JavaScript to define their structure and behavior.
+keywords: custom components v2, component registration, st.components.v2.component, HTML, CSS, JavaScript, ComponentArgs
+---
+
+# Component registration
+
+When you register your component, you define what it looks like and how it behaves:
+
+- To define your component's HTML, CSS, and JavaScript, use [`st.components.v2.component()`](/develop/api-reference/custom-components/st.components.v2.component).
+- In your component's JavaScript code, to send and receive communications with Python, use the properties of the [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) type.
+- In your component's CSS, to make your component theme-aware, use [CSS custom properties](/develop/concepts/custom-components/components-v2/theming#css-custom-properties).
+
+For simplicity and to help you get started with less prerequisite knowledge, this guide uses inline component development. For components that are larger, reusable, or distributed, you should use package-based development. The practical difference is that package-based components let you self-host assets and reference those assets, including component code, by a relative path. Contrastingly, inline components require you to pass raw HTML, CSS, and JavaScript code to your component registration command.
+
+After you learn about custom components with inline development, you can proceed to [package-based development](/develop/concepts/custom-components/components-v2/package-based) where you'll need to understand the basics of [Python packaging](https://packaging.python.org/en/latest/overview/), too.
+
+## Prerequisites
+
+To read this guide, you should be familiar with the basic syntax and concepts of HTML, CSS, and JavaScript. If you want to build an interactive component, you should also know how Streamlit's native widgets work. For more information about Streamlit widgets, see the [Widget behavior](/develop/concepts/architecture/widget-behavior) guide.
+
+## Basic examples
+
+Here are some basic examples of component registration. The parameters are explained in the next section.
+
+### Hello world
+
+This is the [hello world](/develop/concepts/custom-components/components-v2/examples/hello-world) component shown in the quickstart guide. It's a static component that displays "Hello, World!" using the app's theme colors.
+
+```python
+my_component = st.components.v2.component(
+ name="my_hello_world",
+ html="
Hello, World!
",
+ css="h2 { color: var(--st-primary-color); }",
+)
+```
+
+### Simple button
+
+This is the [simple button](/develop/concepts/custom-components/components-v2/examples/simple-button) component shown in the quickstart guide. It's an interactive button that sends a trigger value to Python when clicked.
+
+```python
+my_component = st.components.v2.component(
+ name="my_button",
+ html="",
+ css="button { background: var(--st-primary-color); color: white; }",
+ js="""
+ export default function(component) {
+ const { parentElement, setTriggerValue } = component;
+
+ parentElement.querySelector("button").onclick = () => {
+ setTriggerValue("clicked", true);
+ };
+ }
+ """
+)
+```
+
+## Registration parameters
+
+`name` is a unique identifier for your component. This is used internally by Streamlit to retrieve the HTML, CSS, and JavaScript code when a component instance is mounted. To avoid collisions, Streamlit prefixes component names with the modules they are imported from. For inline components that aren't imported, you must use unique names.
+
+`html`, `css`, and `js` are all optional parameters that define your component's markup, styling, and logic, respectively:
+
+- In the hello world example, `html` contains a single heading element and `css` styles it with the Streamlit theme's primary color. Because it's a static component, it doesn't need any JavaScript logic.
+- In the simple button example, `html` contains a single button element, `css` styles it with the Streamlit theme's primary color, and the default function in `js` listens for clicks and sets a trigger value.
+
+
+
+A component must have either `html`, `js`, or both defined! You can't register a component with only CSS. If you only need to inject CSS, use `st.html()` instead.
+
+
+
+## JavaScript function requirements
+
+Your JavaScript code must export a default function that follows this exact signature:
+
+```javascript
+export default function (component) {
+ // Your component logic here
+
+ return () => {
+ // Cleanup logic (remove event listeners, clear timers, etc.)
+ };
+}
+```
+
+The `component` argument in your exported default function provides essential properties described in the [`ComponentArgs`](/develop/api-reference/custom-components/component-v2-lib-componentargs) type. These properties are typically destructured into local variables for easier access:
+
+```javascript
+export default function (component) {
+ const { name, key, data, parentElement, setStateValue, setTriggerValue } =
+ component;
+ // Your component logic here
+}
+```
+
+- `name` (string): Component name from your Python registration.
+- `key` (string): Unique identifier for a component instance. Use this to assist with tracking unique instances of your component in the DOM, especially if your component acts outside of its `parentElement`.
+- `data` (any): All data passed from Python via the `data` parameter. Use this to customize a component instance or to receive feedback data from your Python backend.
+- `parentElement` (HTMLElement): The DOM element where your component is mounted. Use this to interact with the component's internal DOM elements.
+- `setStateValue` (function): JavaScript function to communicate stateful values to your Python backend. The first argument is the state key name, and the second argument is the value to set.
+- `setTriggerValue` (function): JavaScript function to communicate event-based trigger values to your Python backend. The first argument is the trigger key name, and the second argument is the value to set.
+
+
+
+
+
+
+
+Don't directly overwrite or replace `parentElement.innerHTML`. If you do, you will overwrite the HTML, CSS, and JavaScript code that was registered with the component. If you need to inject content from `data`, do one of the following things:
+
+- Create a placeholder element within `html` to update.
+- Append children to `parentElement`.
+
+
+
+## Using files for inline components
+
+For larger components, you can organize your code into separate files. However, for inline components, you must pass raw HTML, CSS, and JavaScript code when you register them. You can read the files and pass their contents to your inline component. For package-based components, you can pass file references instead. Typically, if you have a component that uses multiple files, package-based components is preferred over inline components.
+
+If you use multiple files for your inline component, use a context manager to read the files and pass their contents to your inline component.
+
+```none filename="Directory structure"
+my_app/
+├── streamlit_app.py
+└── my_component/
+ ├── component.css
+ ├── component.html
+ └── component.js
+```
+
+```python
+# Load HTML, CSS, and JS from external files
+@st.cache_data
+def load_component_code():
+ with open("my_component/my_css.css", "r") as f:
+ CSS = f.read()
+ with open("my_component/my_html.html", "r") as f:
+ HTML = f.read()
+ with open("my_component/my_js.js", "r") as f:
+ JS = f.read()
+ return HTML, CSS, JS
+
+HTML, CSS, JS = load_component_code()
+
+file_component = st.components.v2.component(
+ name="file_based",
+ html=HTML,
+ css=CSS,
+ js=JS,
+)
+```
+
+
+
+Using `@st.cache_data` is a good practice to avoid reloading the component code on every rerun, but you might want to temporarily remove caching during development. Streamlit will automatically invalidate the cache if you make code changes within a cache-decorated function, but Streamlit can't infer the changes from files that are read. In this case, you must manually clear the cache when you make changes to files you've read within the cached function. For more information, see [Caching](/develop/api-reference/caching).
+
+
+
+## Sending values to Python
+
+You can send state and trigger values to Python by calling `setStateValue()` or `setTriggerValue()` in your JavaScript code. For both functions, the first argument is the state or trigger name, and the second argument is the value to set.
+
+```javascript
+setStateValue("count", count);
+setTriggerValue("clicked", true);
+```
+
+Both `setStateValue()` and `setTriggerValue()` trigger a rerun of the script. On the next page, you'll learn about mounting your component, which includes defining callback functions for each state and trigger value. Custom components handle callbacks similarly to native Streamlit widgets, like `st.button()`. However, because components can have multiple states and triggers, a single component instance can have multiple callbacks and also execute multiple callbacks in one script rerun. This is explained in more detail on the next page, [Bidirectional communication](/develop/concepts/custom-components/components-v2/communicate).
diff --git a/content/develop/concepts/custom-components/components-v2/state-and-triggers.md b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md
new file mode 100644
index 000000000..ebd32b418
--- /dev/null
+++ b/content/develop/concepts/custom-components/components-v2/state-and-triggers.md
@@ -0,0 +1,1547 @@
+---
+title: State vs trigger values
+slug: /develop/concepts/custom-components/components-v2/state-and-triggers
+description: Learn the fundamental difference between state and trigger values in Custom Components v2, and when to use each approach for bidirectional communication.
+keywords: custom components v2, state values, trigger values, bidirectional communication, component events, callback functions, setStateValue, setTriggerValue
+---
+
+# State versus trigger values
+
+Custom components v2 provides two distinct mechanisms for frontend-to-backend communication, each designed for different use cases. Understanding when to use state values versus trigger values is crucial for building effective interactive components.
+
+## Two communication patterns
+
+### State values: Persistent data
+
+**Purpose**: Represent the current "state" of your component that persists across reruns.
+
+**When to use**: For values that represent ongoing component state like current selections, input values, or configuration settings.
+
+State values have the following behavior:
+
+- Persist across Streamlit reruns.
+- Accessible via direct property access on the result object and through Session State (when mounted with a key).
+- Updated using `setStateValue(key, value)` in JavaScript.
+
+### Trigger values: Event-based communication
+
+**Purpose**: Signal one-time events or user interactions.
+
+**When to use**: For user actions like clicks, form submissions, or other discrete events.
+
+Trigger values have the following behavior:
+
+- Are transient and only available for one script rerun.
+- Reset to `None` after the rerun completes.
+- Accessible via direct property access on the result object and through Session State (when mounted with a key).
+- Updated using `setTriggerValue(key, value)` in JavaScript.
+
+## Differences at a glance
+
+| Aspect | State values | Trigger values |
+| :------------------ | :-------------------------------------------- | :--------------------------------------- |
+| Persistence | Maintained across reruns | Only available for one rerun |
+| Use case | Current component state | One-time events/actions |
+| JavaScript function | `setStateValue(key, value)` | `setTriggerValue(key, value)` |
+| Callback execution | Only if `setStateValue()` _changed_ the value | Every time `setTriggerValue()` is called |
+
+## State values in practice: Radial menu component
+
+State values are perfect for tracking the ongoing state of your component. Here's a practical example that demonstrates using a state value to track a selection. The following code creates a radial menu component that allows the user to select a food item from a list of options. When the user selects an item, the component updates the state value with `setStateValue("selection", currentSelection)`. You can expand or collapse the each code block as needed. For emphasis, the JavaScript and example app code are expanded by default.
+
+For simplicity, this component assumes it will always have six options in its menu, but with a little more code, you can generalize it accept an arbitrary number of items. The complete code provided at the end of this section demonstrates a generalized version that accepts an arbitrary number of items.
+
+For this example, the component is registered in an imported module.
+
+```
+project_directory/
+├── radial_menu_component/
+│ ├── __init__.py
+│ ├── menu.css
+│ ├── menu.html
+│ └── menu.js
+└── streamlit_app.py
+```
+
+### Radial menu component registration
+
+
+
+```python
+from pathlib import Path
+import streamlit as st
+
+component_dir = Path(__file__).parent
+
+
+@st.cache_data
+def load_component_code():
+ with open(component_dir / "menu.css", "r") as f:
+ CSS = f.read()
+ with open(component_dir / "menu.html", "r") as f:
+ HTML = f.read()
+ with open(component_dir / "menu.js", "r") as f:
+ JS = f.read()
+ return HTML, CSS, JS
+
+
+HTML, CSS, JS = load_component_code()
+
+radial_menu = st.components.v2.component(
+ name="radial_menu",
+ html=HTML,
+ css=CSS,
+ js=JS,
+)
+```
+
+
+
+### Radial menu HTML code
+
+
+
+```markup
+
",
+ css=".my-style { color: red; }", # Won't affect other elements
+ isolate_styles=True # Default
+)
+```
+
+Benefits of isolation:
+
+- Component styles won't leak to the rest of the page.
+- Page styles won't interfere with your component.
+- Safer for third-party components.
+
+### Non-isolated styles
+
+If you want your component's style to affect the rest of the page, you can set `isolate_styles=False`. This is uncommon.
+
+```python
+# Styles can affect the page
+non_isolated_component = st.components.v2.component(
+ name="non_isolated",
+ html="
Content with inheritance
",
+ css=".inherits-styles { font-family: inherit; }", # Inherits page fonts
+ isolate_styles=False
+)
+```
+
+## Responsive design
+
+Create components that work well across different screen sizes. This makes your component more accessible and compatible with the Streamlit layout system. The following example uses `@media (max-width: 768px)` to create a responsive grid layout that adapts when the screen width is less than 768px.
+
+```python
+import streamlit as st
+
+responsive_component = st.components.v2.component(
+ name="responsive_layout",
+ html="""
+