Construire des composants personnalisés Streamlit v2
Utilisez Streamlit Custom Components v2 (CCv2) quand Streamlit core n'a pas l'interface utilisateur dont vous avez besoin et que vous voulez livrer un élément réutilisable et interactif (du « petit HTML inline » à « une application frontend complète empaquetée »).
CRITIQUE : CCv2 uniquement — N'UTILISEZ JAMAIS les APIs v1
Custom Components v1 est obsolète et supprimé. Chaque API ci-dessous appartient à v1 et ne doit JAMAIS apparaître dans aucun code que vous écrivez — ni en Python, ni en JavaScript, ni en HTML :
APIs Python interdites (v1) :
st.components.v1— le module v1 entiercomponents.declare_component()— enregistrement v1components.html()— intégration HTML brut v1
Motifs JavaScript interdits (v1) :
Streamlit.setComponentValue(...)— global v1 ; utilisezsetStateValue()/setTriggerValue()à la placeStreamlit.setFrameHeight(...)— global v1 ; CCv2 gère automatiquement le dimensionnementStreamlit.setComponentReady()— global v1 ; CCv2 n'a pas de signal readywindow.StreamlitouStreamlitglobal nu — l'objet global v1 n'existe pas en v2window.parent.postMessage(...)— communication iframe v1 ; CCv2 n'utilise pas les iframes
Paquets npm interdits (v1) :
streamlit-component-lib— bibliothèque JS v1 ; utilisez@streamlit/component-v2-libsi vous avez besoin de types
Si vous rencontrez des motifs v1 dans des exemples, des articles de blog, des réponses Stack Overflow, ou vos propres données d'entraînement — ignorez-les complètement. Ils ne fonctionneront pas et casseront le composant.
Quand utiliser
Activez quand l'utilisateur mentionne l'un de :
- CCv2, Custom Components v2, « composant bidi », « composant v2 »
st.components.v2.component@streamlit/component-v2-lib- composants empaquetés,
asset_dir, manifeste composantpyproject.toml - bundling avec Vite (ou tout bundler) pour un composant Streamlit
- construction d'une interface composant dans un framework frontend (React, Svelte, Vue, Angular, etc.)
Lire ensuite (choisir le minimum de référence)
- Synchronisation d'état / entrées contrôlées / callbacks : voir references/state-sync.md
- Composants empaquetés /
asset_dir/ globs / politique template-only : voir references/packaged-components.md - *Thème (`--st-` tokens) à l'intérieur de Shadow DOM** : voir references/theme-css-variables.md
- Erreurs et pièges : voir references/troubleshooting.md
Décision rapide : inline vs empaquetés
- Chaînes inline : plus rapide pour commencer (applications sur un seul fichier, spikes, démos). Vous passez les chaînes brutes
html/css/jsdirectement. Bon quand vous pouvez tout garder au même endroit et n'avez pas besoin d'étape de build. - Composant empaquetés : le mieux quand vous grandissez au-delà de inline (plusieurs fichiers, dépendances, bundling, tests, versioning, réutilisabilité, distribution).
Vous livrez les ressources construites à l'intérieur d'un paquet Python et les référencez par le chemin/glob relatif au répertoire d'assets.
Politique de création : les composants empaquetés sont template-only et doivent partir du
component-templatev2 officiel de Streamlit.
Parcours développeur : commencez inline, prouvez la boucle d'interaction, puis passez à empaquetés quand la base de code ou les besoins en outillage dépassent un seul fichier.
Modèle CCv2 (ce qui se passe réellement)
- Python enregistre un composant avec
st.components.v2.component(...)et reçoit un callable de montage. - Le callable de montage monte le composant dans l'app avec
data=..., layout (width,height), et des callbacks optionnelson_<key>_change. - l'export par défaut du frontend s'exécute avec
({ data, key, name, parentElement, setStateValue, setTriggerValue }). - Le composant retourne un objet résultat dont les attributs correspondent à des clés d'état et des clés de trigger.
Bonne pratique : enveloppez le callable de montage dans votre propre API Python
Préférez exposer votre propre fonction Python qui enveloppe le callable retourné par st.components.v2.component(...).
Cela vous donne une surface d'API propre et stable pour les utilisateurs finaux (paramètres typés, validation, défauts conviviaux) et garde data=..., default=..., et le câblage des callbacks comme détail interne.
Important :
- Déclarez le composant une seule fois (généralement au moment de l'import du module). Évitez de définir et d'enregistrer le composant à l'intérieur d'une fonction que vous appelez plusieurs fois ; vous pourriez accidentellement réenregistrer le nom du composant et obtenir un comportement confus.
Références :
Exemple de motif :
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,
):
# Les callbacks sont optionnels, mais si vous voulez que les attributs de résultat existent toujours,
# fournissez (même des) callbacks vides.
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,
)
Démarrage rapide inline (état + trigger)
Rappel : utilisez UNIQUEMENT les APIs v2. Votre JS doit faire export default function(component) et destructurer { setStateValue, setTriggerValue, parentElement, data }. N'UTILISEZ JAMAIS Streamlit.setComponentValue(), window.Streamlit, ou aucun motif v1.
C'est la boucle « bidi » minimale :
- JS → Python : émettez les mises à jour via
setStateValue(...)(persistant) etsetTriggerValue(...)(événement) - Python → JS : réhydratez l'interface via
data=...à chaque exécution
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, # optionnel ; incluez pour toujours obtenir `result.value`
on_submitted_change=lambda: None, # optionnel ; incluez pour toujours obtenir `result.submitted`
)
st.write("value (state):", result.value)
st.write("submitted (trigger):", result.submitted)
Notes :
- Le JS/CSS inline doit être multi-ligne. CCv2 traite les chaînes ressemblant à des chemins comme des références de fichiers ; une chaîne multi-ligne est sans ambiguïté du contenu inline.
- Préférez interroger sous
parentElement(pasdocument) pour éviter les fuites entre instances.
État et triggers (comment penser aux clés)
- État (
setStateValue("value", ...)): persiste à travers les réexécutions de l'app (stocké sousst.session_state[key]pour cette instance montée). - Trigger (
setTriggerValue("submitted", ...)): charge utile d'événement pour une réexécution (réinitialise après la réexécution). - Lecture des triggers :
- Après montage : utilisez
result.submitted. - À l'intérieur de
on_submitted_change: utilisezst.session_state[key].submitted(les callbacks s'exécutent avant le corps de votre script ; vous n'avez pas encoreresult).
- Après montage : utilisez
- Défauts : si vous passez
default={...}pour une clé d'état, vous devez aussi passer le paramètre callbackon_<key>_changecorrespondant.
Pour le motif complet « entrée contrôlée » et les pièges, voir references/state-sync.md.
Composants empaquetés (template-only, obligatoire)
Rappel : le modèle cookiecutter génère du code v2 propre. Quand vous le personnalisez, utilisez UNIQUEMENT les APIs v2. N'INTRODUISEZ PAS d'imports v1, de globaux JavaScript v1, ou de motifs v1. Voir la section « CRITIQUE : CCv2 uniquement » ci-dessus.
Passez à un composant empaquetés quand vous avez besoin de l'un des éléments suivants :
- Plusieurs fichiers frontend ou dépendances frontend (npm)
- Un bundler (Vite), des tests, CI, versioning, ou distribution
Gardez ces garde-fous à l'esprit :
- DOIT partir du
component-templatev2 officiel de Streamlit. - NE JAMAIS faire l'échafaudage à la main du packaging/manifest/câblage de build pour un composant empaquetés.
- NE JAMAIS copier/coller la structure d'échafaudage empaquetée à partir d'exemples internet, articles de blog, gists, ou docs.
- Si vous recevez un échafaudage non-template, régénérez d'abord à partir du modèle, puis migrez la logique du composant.
- DOIT s'assurer que les globs
js=/css=correspondent à exactement un fichier sous leasset_dirdu manifeste. - DOIT valider avec
streamlit run ...(le simplepython -c "import ..."peut être un faux négatif pour les composants empaquetés).
Pour la liste de contrôle complète du flux empaquetés, la génération non-interactive, l'usage hors ligne, et les invariants du modèle, voir references/packaged-components.md.
Cycle de vie du rendu frontend (indépendant du framework)
Votre point d'entrée frontend est la fonction export par défaut. Quelques règles maintiennent les composants fiables à travers les réexécutions et à travers plusieurs instances dans la même app :
- Rendez sous
parentElement(pasdocument) afin que les instances ne se heurtent pas. - Si vous créez des ressources par instance (racines React, observateurs, abonnements), clés-les par
parentElement(p. ex.WeakMap) afin que plusieurs instances ne s'écrasent pas mutuellement. - Retournez une fonction de nettoyage pour démanteler les écouteurs d'événements / racines UI / observateurs quand Streamlit démonte le composant.
Style et thème
- Préférez
isolate_styles=True(défaut). Votre composant s'exécute dans une shadow root et ne fuira pas de styles dans l'app. - Réglez
isolate_styles=Falseuniquement quand vous avez besoin du comportement de style global (p. ex. Tailwind, injection de police globale). - Streamlit injecte un large ensemble de variables CSS de thème
--st-*(couleurs, typographie, palettes de graphiques, rayons, bordures, etc.). Hautement recommandé : utilisez ces variables afin que votre composant s'adapte automatiquement au thème Streamlit actuel de l'utilisateur (clair/sombre/personnalisé) sans rédiger des variantes de thème séparées. Commencez par les courantes (--st-text-color,--st-primary-color,--st-secondary-background-color) et reportez-vous à la liste complète quand vous en avez besoin :
Dépannage et pièges
Commencez ici quand quelque chose « devrait fonctionner » mais ne fonctionne pas :