lib/streamlit/.agents/skills/developing-with-streamlit/references/custom-components-v2.md
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 legacy and superseded by v2. The st.components.v1 module still exists in Streamlit (e.g., declare_component for existing third-party components), but specific v1 APIs like components.v1.html() and components.v1.iframe() are deprecated. For new components, always use CCv2. The following v1 APIs must NEVER appear in new code:
Banned Python APIs (v1 — do not use for new components):
components.declare_component() — v1 registration; use st.components.v2.component() insteadcomponents.v1.html() — deprecated; use st.iframe() insteadcomponents.v1.iframe() — deprecated; use st.iframe() insteadBanned 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 Streamlit global — v1 global object does not exist in v2window.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 ccv2-packaged-components.md--st-* tokens) inside Shadow DOM: see ccv2-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 ccv2-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 ccv2-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: