st.container, st.empty, st.space y st.dialog para modales

Intermedio
Streamlit
Streamlit
Actualizado: 26/04/2026

st.container: agrupación de elementos

st.container es el componente de layout más fundamental de Streamlit. Agrupa elementos relacionados visualmente dentro de un bloque lógico, permitiendo controlar el orden de inserción y crear tarjetas con bordes visibles. A diferencia de st.columns que organiza elementos en horizontal, st.container mantiene el flujo vertical natural pero añade la capacidad de delimitar secciones y controlar la altura con scroll.

Con border=True dibuja un borde alrededor del grupo, creando un efecto visual de "tarjeta" que ayuda al usuario a identificar bloques de contenido relacionado:

import streamlit as st
import numpy as np

# Contenedor sin borde (solo para agrupación lógica)
with st.container():
    st.subheader("Panel de métricas")
    col1, col2, col3 = st.columns(3)
    col1.metric("Ventas", "€ 45.230")
    col2.metric("Clientes", "1.230")
    col3.metric("Conversión", "3,4%")

# Contenedor con borde visible
with st.container(border=True):
    st.subheader("Configuración del modelo")
    st.write("Ajusta los parámetros del modelo antes de entrenar.")
    modelo = st.selectbox("Algoritmo", ["Random Forest", "XGBoost"])
    n_est = st.slider("Estimadores", 50, 500, 100)
    if st.button("Entrenar", type="primary"):
        st.success("Entrenamiento completado.")

# height: contenedor con scroll para contenido largo
with st.container(height=300, border=True):
    st.write("Lista de eventos del sistema:")
    for i in range(20):
        st.write(f"[{i+1:02d}] Evento registrado: acción_usuario_{i+1}")

El parámetro height resulta especialmente útil para listas largas de eventos, logs o cualquier contenido que pueda crecer de forma indefinida: al fijar una altura máxima, el contenedor muestra una barra de scroll vertical en lugar de expandir la página.

flowchart TD
    A[Layout primitives] --> B[st.container agrupa con border]
    A --> C[st.empty placeholder dinámico]
    A --> D[st.space espaciado vertical]
    A --> E[st.dialog modal full screen]
    B --> F[Tarjeta visual con sección]
    C --> G[Reemplazo con nuevo widget]
    G --> H[Stream texto loading bar]
    D --> I[Aire respirable entre bloques]
    E --> J[Confirmación borrar]
    E --> K[Formulario emergente]
    J --> L[st.rerun cierra dialog]
    K --> L
    F --> M[UI ordenada y legible]
    H --> M

st.empty: placeholder de actualización dinámica

st.empty reserva un espacio en la pantalla que puede actualizarse con nuevo contenido en cualquier momento. Cada vez que se llama a un método sobre el placeholder (.write(), .metric(), .info(), etc.), el contenido anterior se reemplaza por el nuevo, sin acumularse. Esto lo convierte en la herramienta principal para crear interfaces con datos que cambian en tiempo real:

import streamlit as st
import time

st.title("Simulación de proceso en tiempo real")

# El placeholder se crea vacío
estado = st.empty()
barra = st.empty()
resultado = st.empty()

if st.button("Iniciar proceso"):
    for i in range(101):
        estado.info(f"Procesando paso {i}/100...")
        barra.progress(i / 100, text=f"{i}%")
        time.sleep(0.05)

    estado.success("¡Proceso completado!")
    barra.empty()  # Limpiar la barra de progreso
    resultado.metric("Resultado final", "98,7%", "+3,2%")

Actualización de múltiples placeholders

import streamlit as st
import time
import random

st.title("Monitor de métricas en tiempo real")

col1, col2, col3 = st.columns(3)
placeholder_cpu = col1.empty()
placeholder_mem = col2.empty()
placeholder_req = col3.empty()

log = st.empty()

if st.button("Iniciar monitoreo (5 segundos)"):
    for _ in range(5):
        cpu = random.uniform(20, 95)
        mem = random.uniform(40, 85)
        req = random.randint(100, 500)

        placeholder_cpu.metric("CPU", f"{cpu:.1f}%", f"{cpu-50:.1f}%")
        placeholder_mem.metric("Memoria", f"{mem:.1f}%", f"{mem-60:.1f}%")
        placeholder_req.metric("Req/s", req, random.randint(-50, 50))

        log.info(f"CPU: {cpu:.1f}% | Mem: {mem:.1f}% | Req/s: {req}")
        time.sleep(1)

    log.success("Monitoreo completado.")

st.space: espacio vertical explícito

import streamlit as st

st.header("Sección A")
st.write("Contenido de la sección A.")

st.space(height=3)  # 3 unidades de espacio vertical

st.header("Sección B")
st.write("Contenido de la sección B.")

@st.dialog: diálogos modales

@st.dialog crea una función que se muestra como un diálogo modal (ventana emergente) al ser llamada:

import streamlit as st

@st.dialog("Confirmar eliminación")
def confirmar_eliminar(nombre):
    st.warning(f"¿Estás seguro de que deseas eliminar **{nombre}**?")
    st.write("Esta acción no se puede deshacer.")
    col1, col2 = st.columns(2)
    with col1:
        if st.button("Eliminar", type="primary", use_container_width=True):
            st.session_state.eliminado = nombre
            st.rerun()
    with col2:
        if st.button("Cancelar", use_container_width=True):
            st.rerun()

@st.dialog("Añadir registro", width="large")
def anadir_registro():
    with st.form("form_nuevo"):
        nombre = st.text_input("Nombre *")
        email = st.text_input("Email *")
        rol = st.selectbox("Rol", ["Analista", "Desarrollador", "Manager"])
        enviado = st.form_submit_button("Guardar", type="primary")
        if enviado and nombre and email:
            if "registros" not in st.session_state:
                st.session_state.registros = []
            st.session_state.registros.append({"nombre": nombre, "email": email, "rol": rol})
            st.rerun()

# Interfaz principal
st.title("Gestión de usuarios")

col1, col2 = st.columns([5, 1])
with col2:
    if st.button("➕ Añadir", type="primary"):
        anadir_registro()

# Mostrar registros con botón de eliminar
registros = st.session_state.get("registros", [
    {"nombre": "Ana García", "email": "ana@empresa.com", "rol": "Analista"},
    {"nombre": "Luis Martín", "email": "luis@empresa.com", "rol": "Desarrollador"}
])

for i, registro in enumerate(registros):
    with st.container(border=True):
        c1, c2, c3, c4 = st.columns([2, 3, 2, 1])
        c1.write(f"**{registro['nombre']}**")
        c2.write(registro["email"])
        c3.write(registro["rol"])
        with c4:
            if st.button("🗑️", key=f"del_{i}"):
                confirmar_eliminar(registro["nombre"])

@st.dialog acepta el parámetro width con valores "small" (por defecto) o "large" para diálogos más anchos.

La función decorada con @st.dialog se ejecuta dentro de un contexto modal aislado. Para cerrar el diálogo, se utiliza st.rerun() que provoca la re-ejecución del script completo, momento en el que el diálogo ya no se muestra (a menos que se vuelva a llamar a la función). Este patrón es el estándar en Streamlit para gestionar el ciclo de vida de los modales.

Contexto: ¿qué problemas resuelven estos componentes?

st.container, st.empty y st.dialog son piezas que parecen auxiliares pero resultan fundamentales para construir interfaces más allá del "dashboard básico". st.container te da control sobre la agrupación visual y sirve como alternativa ligera a los expanders cuando simplemente quieres "encajonar" un conjunto de elementos. st.empty es la herramienta clave para mostrar información en tiempo real: al reemplazar su contenido en lugar de añadir nuevo, puedes construir barras de progreso, monitores de sistema y efectos de streaming sin saturar la página.

st.dialog es el añadido más reciente: antes de él, los modales requerían trucos con CSS inyectado y componentes de terceros. Ahora puedes decorar una función con @st.dialog("título") y llamarla desde cualquier lugar del script para abrirla como ventana emergente. Es ideal para confirmaciones críticas (borrado, cambios irreversibles), formularios secundarios y wizards que no justifican una página entera.

Explicación línea por línea del monitor en tiempo real

  1. Se crean tres placeholders con col.empty(), uno por métrica.
  2. Otro log = st.empty() se reserva para mostrar un mensaje de estado.
  3. El bucle for _ in range(5) simula 5 iteraciones de monitoreo (por ejemplo, 5 segundos).
  4. En cada iteración se calculan valores aleatorios simulando un sistema real.
  5. placeholder_cpu.metric(...) reemplaza el contenido de la celda con el nuevo valor, en lugar de añadir una métrica debajo.
  6. time.sleep(1) espera un segundo antes de la siguiente iteración.
  7. Al salir del bucle, log.success(...) cambia el mensaje de "info" a "success" en el mismo placeholder.

Este patrón es potente porque el script Python sigue ejecutándose secuencialmente y Streamlit actualiza la UI sin bloquear el navegador ni perder el estado de otros widgets.

Tabla de componentes

| Componente | Tipo | Uso principal | |-----------|------|---------------| | st.container | Contexto | Agrupar elementos, crear tarjetas | | st.container(border=True) | Contexto | Tarjeta con borde visible | | st.container(height=N) | Contexto | Contenedor con scroll interno | | st.empty | Placeholder | Actualización dinámica in-place | | st.space | Separador | Añadir espacio vertical | | @st.dialog | Decorador | Crear modales reutilizables | | st.dialog(width="large") | Parámetro | Diálogo ancho para formularios |

Errores comunes

st.empty que no se actualiza. Si llamas varias veces a st.empty() dentro de un bucle, creas un nuevo placeholder por iteración en lugar de reutilizar uno. Instancia el placeholder fuera del bucle y llama a sus métodos dentro.

Diálogos que no se cierran. Si el diálogo no llama a st.rerun() al finalizar, quedará abierto en un estado intermedio. Añade siempre un st.rerun() al final del manejador de botón de cierre o confirmación.

Múltiples diálogos simultáneos. Streamlit solo puede mostrar un diálogo a la vez. Si llamas a dos funciones @st.dialog en la misma ejecución, solo se verá la última.

Contenedores con altura fija y gráficos. Un st.container(height=300) puede recortar gráficos Plotly o Matplotlib si estos son más altos que 300 píxeles. Ajusta la altura del gráfico con fig.update_layout(height=280) para que encaje.

st.empty en lugar de st.fragment. Para actualizaciones que ocurren en respuesta a eventos del usuario (no en bucles), st.fragment es más idiomático que st.empty. Reserva st.empty para loops de streaming y actualizaciones temporizadas.

Mejores prácticas

  • Usa st.container(border=True) para crear tarjetas visuales de KPIs sin necesidad de HTML custom.
  • Combina st.empty con time.sleep solo para demos cortas; para polling real, considera st.fragment(run_every=N).
  • Los diálogos modales deben ser breves: si el formulario tiene más de 10 campos, es mejor usar una página dedicada.
  • Para confirmaciones de acciones destructivas, usa @st.dialog con dos botones claramente diferenciados ("Confirmar" en rojo, "Cancelar" en gris).
  • Documenta los diálogos como funciones con un nombre descriptivo (confirmar_eliminar_usuario) para que el código sea autoexplicativo.
  • Recuerda que st.empty acepta cualquier método de Streamlit, incluyendo st.dataframe, st.plotly_chart y st.markdown. Explota esta versatilidad para layouts dinámicos sofisticados.
Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Streamlit es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Streamlit

Explora más contenido relacionado con Streamlit y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Agrupar elementos visualmente con st.container y su parámetro border. Usar st.empty como placeholder para actualización dinámica de contenido. Controlar el espaciado vertical con st.space. Crear diálogos modales con el decorador @st.dialog para confirmaciones y formularios. Aplicar patrones de actualización en tiempo real con st.empty y st.fragment.