Custom components

Streamlit · Expert · 14 min

When no built-in element or community component fits, a custom component adds any web UI to your app.

There are three levels of customization.

Static HTML: st.html(markup) inserts HTML (and CSS in <style> tags) into the page without an iframe; scripts are not executed by default. st.markdown(..., unsafe_allow_html=True) allows inline HTML in Markdown. Good for badges, custom styling and small visual tweaks.

Bidirectional components (v2): st.components.v2.component(name, html=..., css=..., js=...) registers a component and returns a mount function. The JavaScript exports a default function that receives a component object with:

  • data: values passed from Python when mounting;
  • parentElement: where to render (a shadow root when isolate_styles=True, the default);
  • setStateValue(key, value): persistent state, available in Python as an attribute of the result and in session state;
  • setTriggerValue(key, value): a one-shot event, like a button click.

In Python, result = my_component(data=..., key="c1", on_clicked_change=callback) returns the component's state; passing an on_<key>_change callback opts into that value. Packaged components can ship asset files declared in pyproject.toml.

Legacy components (v1): components.v1.html renders HTML in a sandboxed iframe, and declare_component with a React or plain-JS front end built separately communicates through Streamlit.setComponentValue. Many community packages still use v1.

Components are trusted code: Streamlit does not sanitize their HTML or JS. Never inject user input, query parameters or LLM output into them without escaping.

Example

import streamlit as st

JS = '''
export default function(component) {
  const { data, parentElement, setTriggerValue, setStateValue } = component;
  const box = document.createElement("div");
  box.innerHTML = data.options
    .map(o => `<button data-v="${o}">${o}</button>`).join(" ");
  box.querySelectorAll("button").forEach(b => {
    b.onclick = () => { setStateValue("choice", b.dataset.v);
                        setTriggerValue("clicked", b.dataset.v); };
  });
  parentElement.appendChild(box);
}
'''
picker = st.components.v2.component("choice_picker", js=JS,
                                    css="button { margin: 2px; }")

result = picker(data={"options": ["small", "medium", "large"]}, key="size",
                on_choice_change=lambda: None, on_clicked_change=lambda: None)
st.write("Persistent choice:", result.choice)
if result.clicked:
    st.toast(f"Clicked {result.clicked}")
st.html("<span style='color:#E8383D'>Static HTML, no iframe</span>")

Remember

  • st.html inserts static HTML/CSS without an iframe
  • st.components.v2.component registers HTML/CSS/JS with state and trigger values
  • Components are trusted code: escape anything user-provided

Watch out for

  • Interpolating user input or LLM output into a component's HTML, creating a cross-site scripting hole.

Check yourself

  1. In a v2 component, which JavaScript call sends a one-off event (like a click) to Python rather than persistent state?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.streamlit.io/develop/concepts/custom-components