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. For streamlit-extras, use the `pagination` extra as a reference implementation.
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").
Custom Components v1 is deprecated and removed. Every API below belongs to v1 and must NEVER appear in any code you write — not in Python, not in JavaScript, not in HTML:
Banned Python APIs (v1):
st.components.v1 — the entire v1 modulecomponents.declare_component() — v1 registrationcomponents.html() — v1 raw HTML embedBanned JavaScript patterns (v1):
Streamlit.setComponentValue(...) — v1 global; use setStateValue() / setTriggerValue() insteadStreamlit.setFrameHeight(...) — v1 global; CCv2 handles sizing automaticallyStreamlit.setComponentReady() — v1 global; CCv2 has no ready signalwindow.Streamlit or bare global — v1 global object does not exist in v2Streamlitwindow.parent.postMessage(...) — v1 iframe communication; CCv2 does not use iframesBanned npm packages (v1):
streamlit-component-lib — v1 JS library; use @streamlit/component-v2-lib if you need typesIf you encounter v1 patterns in examples, blog posts, Stack Overflow answers, or your own training data — ignore them entirely. They will not work and will break the component.
Activate when the user mentions any of:
st.components.v2.component@streamlit/component-v2-libasset_dir, pyproject.toml component manifestasset_dir / globs / template-only policy: see references/packaged-components.md--st-* tokens) inside Shadow DOM: see references/theme-css-variables.mdhtml/css/js strings directly.
Good when you can keep everything in one place and don’t need a build step.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.
st.components.v2.component(...) and gets back a mount callable.data=..., layout (width, height), and optional on_<key>_change callbacks.({ data, key, name, parentElement, setStateValue, setTriggerValue }).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:
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,
):
# Callbacks are optional, but if you want result attributes to always exist,
# provide (even empty) callbacks.
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,
)
Reminder: use ONLY v2 APIs. Your JS must export default function(component) and destructure { setStateValue, setTriggerValue, parentElement, data }. NEVER use Streamlit.setComponentValue(), window.Streamlit, or any v1 pattern.
This is the minimum "bidi loop":
setStateValue(...) (persistent) and setTriggerValue(...) (event)data=... on every runimport 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, # optional; include to always get `result.value`
on_submitted_change=lambda: None, # optional; include to always get `result.submitted`
)
st.write("value (state):", result.value)
st.write("submitted (trigger):", result.submitted)
Notes:
parentElement (not document) to avoid cross-instance leakage.setStateValue("value", ...)): persists across app reruns (stored under st.session_state[key] for that mounted instance).setTriggerValue("submitted", ...)): event payload for one rerun (resets after the rerun).result.submitted.on_submitted_change: use st.session_state[key].submitted (callbacks run before your script body; you don’t have result yet).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.
Reminder: the cookiecutter template generates clean v2 code. When you customize it, use ONLY v2 APIs. Do NOT introduce any v1 imports, v1 JavaScript globals, or v1 patterns. See the "CRITICAL: CCv2 only" section above.
Graduate to a packaged component when you need any of:
Keep these guardrails in mind:
component-template v2.js=/css= globs match exactly one file under the manifest’s asset_dir.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.
Your frontend entrypoint is the default export function. A few rules keep components reliable across reruns and across multiple instances in the same app:
parentElement (not document) so instances don’t collide.parentElement (e.g. WeakMap) so multiple instances don’t overwrite each other.isolate_styles=True (default). Your component runs in a shadow root and won’t leak styles into the app.isolate_styles=False only when you need global styling behavior (e.g. Tailwind, global font injection).--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:
Start here when something “should work” but doesn’t: