building-streamlit-custom-components-v2

Par streamlit · agent-skills

Construit des Streamlit Custom Components v2 (CCv2) bidirectionnels à l'aide de `st.components.v2.component`. À utiliser lors de la création de composants HTML/CSS/JS inline ou de composants packagés (manifest `asset_dir`, globs js/css), du câblage de callbacks state/trigger, du theming via les variables CSS `--st-*`, ou du bundling avec Vite / `component-template` v2.

npx skills add https://github.com/streamlit/agent-skills --skill building-streamlit-custom-components-v2

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 entier
  • components.declare_component() — enregistrement v1
  • components.html() — intégration HTML brut v1

Motifs JavaScript interdits (v1) :

  • Streamlit.setComponentValue(...) — global v1 ; utilisez setStateValue() / setTriggerValue() à la place
  • Streamlit.setFrameHeight(...) — global v1 ; CCv2 gère automatiquement le dimensionnement
  • Streamlit.setComponentReady() — global v1 ; CCv2 n'a pas de signal ready
  • window.Streamlit ou Streamlit global nu — l'objet global v1 n'existe pas en v2
  • window.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-lib si 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 composant pyproject.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)

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/js directement. 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-template v2 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)

  1. Python enregistre un composant avec st.components.v2.component(...) et reçoit un callable de montage.
  2. Le callable de montage monte le composant dans l'app avec data=..., layout (width, height), et des callbacks optionnels on_<key>_change.
  3. l'export par défaut du frontend s'exécute avec ({ data, key, name, parentElement, setStateValue, setTriggerValue }).
  4. 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) et setTriggerValue(...) (é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 (pas document) 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é sous st.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 : utilisez st.session_state[key].submitted (les callbacks s'exécutent avant le corps de votre script ; vous n'avez pas encore result).
  • Défauts : si vous passez default={...} pour une clé d'état, vous devez aussi passer le paramètre callback on_<key>_change correspondant.

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-template v2 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 le asset_dir du manifeste.
  • DOIT valider avec streamlit run ... (le simple python -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 (pas document) 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=False uniquement 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 :

Skills similaires