Awesome-omni-skill building-streamlit-custom-components-v2
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.
git clone https://github.com/diegosouzapw/awesome-omni-skill
T=$(mktemp -d) && git clone --depth=1 https://github.com/diegosouzapw/awesome-omni-skill "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/development/building-streamlit-custom-components-v2" ~/.claude/skills/diegosouzapw-awesome-omni-skill-building-streamlit-custom-components-v2-e07bac && rm -rf "$T"
skills/development/building-streamlit-custom-components-v2/SKILL.mdBuilding 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").
CRITICAL: CCv2 only — NEVER use v1 APIs
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):
— the entire v1 modulest.components.v1
— v1 registrationcomponents.declare_component()
— v1 raw HTML embedcomponents.html()
Banned JavaScript patterns (v1):
— v1 global; useStreamlit.setComponentValue(...)
/setStateValue()
insteadsetTriggerValue()
— v1 global; CCv2 handles sizing automaticallyStreamlit.setFrameHeight(...)
— v1 global; CCv2 has no ready signalStreamlit.setComponentReady()
or barewindow.Streamlit
global — v1 global object does not exist in v2Streamlit
— v1 iframe communication; CCv2 does not use iframeswindow.parent.postMessage(...)
Banned npm packages (v1):
— v1 JS library; usestreamlit-component-lib
if you need types@streamlit/component-v2-lib
If 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.
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
component manifestpyproject.toml - 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)
- State sync / controlled inputs / callbacks: see references/state-sync.md
- Packaged components /
/ globs / template-only policy: see references/packaged-components.mdasset_dir - Theming (
tokens) inside Shadow DOM: see references/theme-css-variables.md--st-* - Errors and gotchas: see references/troubleshooting.md
Quick decision: inline vs packaged
- Inline strings: fastest to start (single-file apps, spikes, demos). You pass raw
/html
/css
strings directly. Good when you can keep everything in one place and don’t need a build step.js - 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
v2.component-template
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
and gets back a mount callable.st.components.v2.component(...) - The mount callable mounts the component in the app with
, layout (data=...
,width
), and optionalheight
callbacks.on_<key>_change - 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, ): # 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, )
Inline quickstart (state + trigger)
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":
- JS → Python: emit updates via
(persistent) andsetStateValue(...)
(event)setTriggerValue(...) - Python → JS: re-hydrate UI via
on every rundata=...
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, # 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:
- 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
(notparentElement
) to avoid cross-instance leakage.document
State and triggers (how to think about keys)
- State (
): persists across app reruns (stored undersetStateValue("value", ...)
for that mounted instance).st.session_state[key] - Trigger (
): event payload for one rerun (resets after the rerun).setTriggerValue("submitted", ...) - Reading triggers:
- After mounting: use
.result.submitted - Inside
: useon_submitted_change
(callbacks run before your script body; you don’t havest.session_state[key].submitted
yet).result
- After mounting: use
- Defaults: if you pass
for a state key, you must also pass the matchingdefault={...}
callback parameter.on_<key>_change
For the full “controlled input” pattern and pitfalls, see references/state-sync.md.
Packaged components (template-only, mandatory)
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:
- 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
v2.component-template - 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=
globs match exactly one file under the manifest’scss=
.asset_dir - MUST validate with
(plainstreamlit run ...
can be a false negative for packaged components).python -c "import ..."
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
(notparentElement
) so instances don’t collide.document - If you create per-instance resources (React roots, observers, subscriptions), key them by
(e.g.parentElement
) so multiple instances don’t overwrite each other.WeakMap - Return a cleanup function to tear down event listeners / UI roots / observers when Streamlit unmounts the component.
Styling and theming
- Prefer
(default). Your component runs in a shadow root and won’t leak styles into the app.isolate_styles=True - Set
only when you need global styling behavior (e.g. Tailwind, global font injection).isolate_styles=False - Streamlit injects a broad set of
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-*
,--st-text-color
,--st-primary-color
) and refer to the full list when you need it:--st-secondary-background-color
Troubleshooting and gotchas
Start here when something “should work” but doesn’t: