| name | building-streamlit-custom-components-v2 |
| description | Builds bidirectional Streamlit Custom Components v2 (CCv2) using `st.components.v2.component`. Use when authoring inline HTML/CSS/JS components or packaged components (manifest `asset_dir`, js/css globs), wiring state/trigger callbacks, theming via `--st-*` CSS variables, or bundling with Vite / `component-template` v2. |
| license | Apache-2.0 |
| compatibility | Requires Streamlit with Custom Components v2 (`st.components.v2`). Packaged components require Node.js + npm; examples use `uv` + `cookiecutter` for project generation and editable installs. |
Building Streamlit custom components v2
Use Streamlit Custom Components v2 (CCv2) when core Streamlit doesnât have the UI you need and you want to ship a reusable, interactive element (from âtiny inline HTMLâ to âfull bundled frontend appâ).
When to use
Activate when the user mentions any of:
- CCv2, Custom Components v2, âbidi componentâ, âcomponent v2â
st.components.v2.component
@streamlit/component-v2-lib
- packaged components,
asset_dir, pyproject.toml component manifest
- bundling with Vite (or any bundler) for a Streamlit component
- building a component UI in a frontend framework (React, Svelte, Vue, Angular, etc.)
Read next (pick the minimum reference)
Quick decision: inline vs packaged
- Inline strings: fastest to start (single-file apps, spikes, demos). You pass raw
html/css/js strings directly.
Good when you can keep everything in one place and donât need a build step.
- Packaged component: best when youâre growing past inline (multiple files, dependencies, bundling, testing, versioning, reuse, distribution).
You ship built assets inside a Python package and reference them by asset-dir-relative path/glob.
Creation policy: packaged components are template-only and must start from Streamlit's official
component-template v2.
Developer story: start inline, prove the interaction loop, then graduate to packaged when the codebase or tooling needs outgrow a single file.
CCv2 model (whatâs actually happening)
- Python registers a component with
st.components.v2.component(...) and gets back a mount callable.
- The mount callable mounts the component in the app with
data=..., layout (width, height), and optional on_<key>_change callbacks.
- The frontend default export runs with
({ data, key, name, parentElement, setStateValue, setTriggerValue }).
- The component returns a result object whose attributes correspond to state keys and trigger keys.
Best practice: wrap the mount callable in your own Python API
Prefer exposing your own Python function that wraps the callable returned by st.components.v2.component(...).
This gives you a clean, stable API surface for end users (typed parameters, validation, friendly defaults) and keeps data=..., default=..., and callback wiring as an internal detail.
Important:
- Declare the component once (usually at module import time). Avoid defining and registering the component inside a function you call multiple times; you can accidentally re-register the component name and get confusing behavior.
References:
Example pattern:
import streamlit as st
from collections.abc import Callable
_MY_COMPONENT = st.components.v2.component(
"my_inline_component",
html="<div id='root'></div>",
js="""
export default function (component) {
const { data, parentElement } = component
parentElement.querySelector("#root").textContent = data?.label ?? ""
}
""",
)
def my_component(
label: str,
*,
key: str | None = None,
on_value_change: Callable[[], None] | None = None,
on_submitted_change: Callable[[], None] | None = None,
):
if on_value_change is None:
on_value_change = lambda: None
if on_submitted_change is None:
on_submitted_change = lambda: None
return _MY_COMPONENT(
data={"label": label},
key=key,
on_value_change=on_value_change,
on_submitted_change=on_submitted_change,
)
Inline quickstart (state + trigger)
This is the minimum âbidi loopâ:
- JS â Python: emit updates via
setStateValue(...) (persistent) and setTriggerValue(...) (event)
- Python â JS: re-hydrate UI via
data=... on every run
import streamlit as st
HTML = """<input id="txt" /><button id="btn" type="button">Submit</button>"""
JS = """\
export default function (component) {
const { data, parentElement, setStateValue, setTriggerValue } = component
const input = parentElement.querySelector("#txt")
const btn = parentElement.querySelector("#btn")
if (!input || !btn) return
const nextValue = (data && data.value) ?? ""
if (input.value !== nextValue) input.value = nextValue
input.oninput = (e) => {
setStateValue("value", e.target.value)
}
btn.onclick = () => {
setTriggerValue("submitted", input.value)
}
}
"""
my_text_input = st.components.v2.component(
"my_inline_text_input",
html=HTML,
js=JS,
)
KEY = "txt-1"
component_state = st.session_state.get(KEY, {})
value = component_state.get("value", "")
result = my_text_input(
key=KEY,
data={"value": value},
on_value_change=lambda: None,
on_submitted_change=lambda: None,
)
st.write("value (state):", result.value)
st.write("submitted (trigger):", result.submitted)
Notes:
- Inline JS/CSS should be multi-line. CCv2 treats path-like strings as file references; a multi-line string is unambiguously inline content.
- Prefer querying under
parentElement (not document) to avoid cross-instance leakage.
State and triggers (how to think about keys)
- State (
setStateValue("value", ...)): persists across app reruns (stored under st.session_state[key] for that mounted instance).
- Trigger (
setTriggerValue("submitted", ...)): event payload for one rerun (resets after the rerun).
- Reading triggers:
- After mounting: use
result.submitted.
- Inside
on_submitted_change: use st.session_state[key].submitted (callbacks run before your script body; you donât have result yet).
- Defaults: if you pass
default={...} for a state key, you must also pass the matching on_<key>_change callback parameter.
For the full âcontrolled inputâ pattern and pitfalls, see references/state-sync.md.
Packaged components (template-only, mandatory)
Graduate to a packaged component when you need any of:
- Multiple frontend files or frontend dependencies (npm)
- A bundler (Vite), tests, CI, versioning, or distribution
Keep these guardrails in mind:
- MUST start from Streamlitâs official
component-template v2.
- NEVER hand-scaffold packaging/manifest/build wiring for a packaged component.
- NEVER copy/paste packaged scaffold structure from internet examples, blog posts, gists, or docs.
- If handed a non-template scaffold, regenerate from the template first, then migrate component logic.
- MUST ensure
js=/css= globs match exactly one file under the manifestâs asset_dir.
- MUST validate with
streamlit run ... (plain python -c "import ..." can be a false negative for packaged components).
For the full packaged workflow checklist, non-interactive generation, offline usage, and template invariants, see references/packaged-components.md.
Frontend renderer lifecycle (framework-agnostic)
Your frontend entrypoint is the default export function. A few rules keep components reliable across reruns and across multiple instances in the same app:
- Render under
parentElement (not document) so instances donât collide.
- If you create per-instance resources (React roots, observers, subscriptions), key them by
parentElement (e.g. WeakMap) so multiple instances donât overwrite each other.
- Return a cleanup function to tear down event listeners / UI roots / observers when Streamlit unmounts the component.
Styling and theming
- Prefer
isolate_styles=True (default). Your component runs in a shadow root and wonât leak styles into the app.
- Set
isolate_styles=False only when you need global styling behavior (e.g. Tailwind, global font injection).
- Streamlit injects a broad set of
--st-* theme CSS variables (colors, typography, chart palettes, radii, borders, etc.). Highly recommended: use these variables so your component automatically adapts to the userâs current Streamlit theme (light/dark/custom) without authoring separate theme variants. Start with the common ones (--st-text-color, --st-primary-color, --st-secondary-background-color) and refer to the full list when you need it:
Troubleshooting and gotchas
Start here when something âshould workâ but doesnât: