Cómo Añadir Múltiples Páginas a NiceGUI (Guía 2026)
Aprende a añadir múltiples páginas a tu app de NiceGUI con enrutado @ui.page(), layouts compartidos y navegación SPA.
Actualizado Publicado 19 min de lectura

NiceGUI (más de 16k estrellas en GitHub) es un framework de UI en Python para construir interfaces web en Python, sin necesidad de JavaScript. La mayoría de las apps que van más allá de un prototipo rápido necesitan múltiples páginas con navegación compartida: un header, una barra lateral y contenido que cambia según la URL.
Esta guía cubre tres enfoques para el enrutado de páginas en NiceGUI: el clásico @ui.page() con un frame compartido, la navegación estilo SPA con ui.sub_pages y el enrutado modular con APIRouter. Elige el que se ajuste a la complejidad de tu app.
Actualizado para NiceGUI v3.15
Esta guía se actualizó para NiceGUI v3.x (probada con v3.15.0). Si migras desde v1.x o v2.x, consulta la guía oficial de migración a v3. Cambios clave: se requiere Python 3.10+, se eliminó la API .tailwind, ui.open() fue reemplazado por ui.navigate.to() y el cliente compartido auto-index ya no existe.
Si eres nuevo en NiceGUI, empieza con la guía de primeros pasos con NiceGUI. Si estás evaluando frameworks de UI en Python, la comparación Streamlit vs NiceGUI o nuestro resumen de los mejores frameworks web de Python pueden ayudarte a decidir.
El vídeo de abajo muestra el enfoque original con NiceGUI v1.x. Los patrones de código siguen aplicando. El enfoque de @ui.page() + frame con @contextmanager sigue siendo el patrón idiomático de NiceGUI. Consulta las notas específicas de v3 a lo largo de esta guía.
Requisitos previos
Antes de empezar, asegúrate de tener lo siguiente:
- Python 3.10 o más reciente (
python3 --versionpara comprobarlo) - pip (incluido con Python) o configura un proyecto Python con uv para instalaciones más rápidas
- Un editor de texto o IDE (se recomienda VS Code con la extensión de NiceGUI para el autocompletado de Tailwind)
- ~10 minutos de tiempo
Instala NiceGUI fijando la versión:
pip install "nicegui>=3.0.0"O con uv:
uv pip install "nicegui>=3.0.0"Verifica la instalación:
pip show niceguiDeberías ver Version: 3.x.x en la salida. Si ves una versión inferior a 3.0, actualiza con pip install --upgrade nicegui.
Estructura del proyecto para una app NiceGUI multipágina
Mantener los archivos organizados desde el principio te ahorra dolores de cabeza cuando añadas más páginas después. Esta es la estructura de carpetas que vamos a construir:
my_nicegui_app/
├── main.py # Entry point — starts the server
├── theme.py # Shared layout frame (header, drawer, footer)
├── menu.py # Navigation menu links
├── home_page.py # Home page content
├── all_pages.py # Route registration
├── pages/
│ ├── __init__.py # Empty — makes pages a proper Python package
│ ├── title_generator.py
│ └── script_generator.py
├── requirements.txt
└── .gitignoreCada archivo tiene una sola tarea. theme.py define el layout compartido. menu.py define los enlaces de navegación. La carpeta pages/ contiene el contenido de cada página. all_pages.py conecta las rutas. main.py lo arranca todo.
Aviso sobre el repo de inicio
El repo bitdoze/nicegui-starter está pensado para NiceGUI v1.x. Usa el código de esta guía para compatibilidad con v3.x.
Dependencias y fijado de versiones
Fija la versión mayor en requirements.txt. Quita numpy a menos que la uses (las páginas de ejemplo de aquí no la usan):
nicegui>=3.0.0No te saltes pages/__init__.py. Puede ser un archivo vacío, pero es necesario para un empaquetado correcto de Python. Herramientas como PyInstaller y Nuitka lo requieren.
Enfoque 1: enrutado de páginas con @ui.page() y un frame compartido
Este es el patrón de enrutado de páginas “tradicional” de NiceGUI. Cada URL tiene su propio decorador @ui.page(), y todas comparten un layout mediante un frame con @contextmanager. Úsalo cuando:
- Quieres instancias de página independientes (cada cliente recibe un árbol de UI nuevo)
- Las páginas tienen layouts muy diferentes
- Estás construyendo un sitio multipágina sencillo con unas pocas rutas
El frame con @contextmanager sigue siendo el patrón idiomático de NiceGUI para layouts compartidos. El ejemplo oficial de modularización usa el mismo enfoque.
Creando el layout compartido (theme.py)
Este archivo define el frame de la página: header, drawer lateral, footer y el área de contenido. Cada página llama a theme.frame() y coloca su contenido dentro.
from contextlib import contextmanager
from menu import menu
from nicegui import ui
@contextmanager
def frame(navtitle: str):
"""Custom page frame to share the same styling and behavior across all pages"""
ui.colors(primary='#6E93D6', secondary='#53B689', accent='#111B1E', positive='#53B689')
with ui.column().classes('absolute-center items-center h-screen no-wrap p-9 w-full'):
yield
with ui.header() as header:
ui.button(on_click=lambda: left_drawer.toggle(), icon='menu').props('flat color=white')
ui.label('Getting Started').classes('font-bold')
with ui.footer(value=False) as footer:
ui.label('Footer')
with ui.left_drawer().classes('bg-blue-100') as left_drawer:
ui.label('Menu')
with ui.column():
menu()
with ui.page_sticky(position='bottom-right', x_offset=20, y_offset=20):
ui.button(on_click=footer.toggle, icon='contact_support').props('fab')La sentencia yield es donde se inserta el contenido de la página. El decorador @contextmanager lo hace reutilizable. Cada página llama a with theme.frame('Page Title'): y coloca sus elementos de UI dentro del bloque.
Puntos clave:
ui.colors()define el esquema de color de toda la páginaui.header()crea la barra superior con un botón de menú hamburguesaui.left_drawer()crea una barra lateral deslizante con navegaciónui.footer()crea una barra inferior opcional (oculta por defecto, se alterna con el botón FAB)
Construyendo el menú de navegación (menu.py)
from nicegui import ui
def menu() -> None:
ui.link('Home', '/').classes('text-black')
ui.link('YouTube Titles', '/youtube-title-generator/').classes('text-black')
ui.link('YouTube Script Generator', '/youtube-script/').classes('text-black')Cada ui.link() crea un enlace de navegación clicable. El primer argumento es el texto visible, el segundo es la ruta URL. Para navegación programática (p. ej., tras enviar un formulario), usa ui.navigate.to('/new-page') en lugar de enlaces.
Definiendo el contenido de las páginas (pages/)
Cada archivo de página define una función que usa el context manager theme.frame(). Toda la creación de UI ocurre dentro de la función, nunca a nivel de módulo (esto es un requisito estricto en NiceGUI v3).
pages/title_generator.py
import theme
from nicegui import ui
def title_generator():
with theme.frame('YouTube Title Generator'):
ui.page_title('YouTube Title Generator')
ui.markdown('# Title Generator')
ui.markdown('Generate catchy titles for your YouTube videos.')pages/script_generator.py
import theme
from nicegui import ui
def script_generator():
with theme.frame('YouTube Script Generator'):
ui.page_title('YouTube Script Generator')
ui.markdown('# Script Generator')
ui.markdown('Create video scripts with AI assistance.')No olvides pages/__init__.py. Simplemente crea un archivo vacío. Sin él, Python no tratará pages como un paquete.
Registrando las rutas (all_pages.py)
Este archivo mapea las rutas URL a funciones de página usando decoradores ui.page():
from nicegui import ui
from pages.title_generator import title_generator
from pages.script_generator import script_generator
def create() -> None:
@ui.page('/youtube-title-generator/')
def title_page():
title_generator()
@ui.page('/youtube-script/')
def script_page():
script_generator()La función create() se llama una vez desde main.py para registrar todas las rutas antes de que arranque el servidor.
Juntándolo todo (main.py)
import all_pages
import home_page
import theme
from nicegui import ui
@ui.page('/')
def index_page() -> None:
with theme.frame('Homepage'):
home_page.content()
all_pages.create()
ui.run(title='Getting Started With NiceGUI')Verifica: Ejecuta python main.py y abre http://localhost:8080. Deberías ver el header con un menú hamburguesa, la barra lateral con los enlaces de navegación y el contenido de la página de inicio. Haz clic en cada enlace del menú para confirmar que la navegación funciona.
Error: 'ui.page cannot be used in the global scope'
Esto ocurre cuando tienes creación de UI a nivel de módulo junto a decoradores @ui.page(). NiceGUI v3 eliminó el cliente compartido auto-index. TODA la UI debe estar dentro de funciones @ui.page(). Mueve cualquier llamada de nivel superior a ui.element(), ui.label(), etc. dentro de una función de página.
Enfoque 2: navegación SPA con ui.sub_pages
ui.sub_pages es el enfoque moderno de NiceGUI para apps donde quieres navegación del lado del cliente. El header y la barra lateral permanecen fijos, y solo cambia el área de contenido. Sin recarga completa de página, sin parpadeo de contenido sin estilos.
Esto es lo que la mayoría de los lectores que construyen dashboards o herramientas internas realmente quieren.
Cuándo usar sub_pages vs @ui.page()
sub_pages (SPA)
- Cambio de contenido del lado del cliente, sin recarga de página
- El header y la barra lateral persisten entre navegaciones
- La URL se actualiza en la barra del navegador
- Ideal para dashboards y herramientas internas con navegación consistente
- Añadido en NiceGUI v2.22.0, estable en v3
@ui.page() (tradicional)
- Recarga completa de página por ruta
- Cada página obtiene una instancia de cliente independiente
- Las páginas pueden tener layouts completamente diferentes
- Mejor para sitios públicos con diseños de página distintos
- Ha sido el enfoque estándar desde NiceGUI 1.x
Ejemplo completo de sub_pages
Aquí tienes una app SPA autocontenida. Todo va en main.py por claridad. En un proyecto real, separarías las funciones de página en archivos distintos:
from nicegui import ui
def menu():
ui.link('Home', '/').classes('text-black')
ui.link('YouTube Titles', '/youtube-title-generator/').classes('text-black')
ui.link('YouTube Script Generator', '/youtube-script/').classes('text-black')
def home():
ui.markdown('# Welcome')
ui.markdown('Select a tool from the sidebar.')
def title_generator():
ui.markdown('# Title Generator')
ui.markdown('Generate catchy titles for your YouTube videos.')
def script_generator():
ui.markdown('# Script Generator')
ui.markdown('Create video scripts with AI assistance.')
@ui.page('/')
def root():
ui.colors(primary='#6E93D6', secondary='#53B689', accent='#111B1E', positive='#53B689')
with ui.header():
ui.button(on_click=lambda: left_drawer.toggle(), icon='menu').props('flat color=white')
ui.label('My App').classes('font-bold')
with ui.left_drawer().classes('bg-blue-100') as left_drawer:
with ui.column():
menu()
with ui.column().classes('p-4 w-full'):
ui.sub_pages({
'/': home,
'/youtube-title-generator/': title_generator,
'/youtube-script/': script_generator,
})
ui.run(title='My App')Verifica: Ejecuta la app. Haz clic en los enlaces del menú. La URL debería cambiar y el área de contenido debería intercambiarse sin una recarga completa de página. El header y la barra lateral permanecen fijos. Usa el botón de retroceso del navegador para confirmar que la navegación del historial funciona.
¿Por qué sub_pages muestra una página en blanco?
Asegúrate de que las claves del diccionario de rutas en ui.sub_pages() coincidan exactamente con las rutas de los enlaces del menú, incluidas las barras finales. La ruta raíz debe ser '/'. Verifica también que las rutas de ui.link() del menú coincidan con las claves del diccionario carácter por carácter.
Enfoque 3: enrutado modular con APIRouter
Para apps más grandes donde quieres agrupar páginas relacionadas bajo un prefijo de URL y mantenerlas en módulos separados, nicegui.APIRouter funciona como el APIRouter de FastAPI. Escala bien cuando tienes muchos grupos de funcionalidades y varios desarrolladores.
Agrupando páginas con prefijos de URL compartidos
api_pages.py
from nicegui import APIRouter, ui
router = APIRouter(prefix='/tools')
@router.page('/')
def tools_index():
ui.markdown('# Tools')
ui.markdown('Select a tool from the list.')
@router.page('/generator/{name}')
def generator(name: str):
ui.markdown(f'# Generator: {name}')
ui.markdown(f'This is the {name} generator page.')main.py
import api_pages
from nicegui import app, ui
@ui.page('/')
def index():
ui.markdown('# Home')
ui.link('Go to Tools', '/tools/')
app.include_router(api_pages.router)
ui.run(title='APIRouter Example')Verifica: Ejecuta la app. Visita /tools/ para confirmar que carga el índice de herramientas. Visita /tools/generator/title para confirmar que el parámetro de ruta dinámico funciona (deberías ver “Generator: title”).
Limitación de NiceGUI On Air
APIRouter no funciona actualmente con NiceGUI On Air (el servicio de despliegue alojado). Usa el Enfoque 1 o 2 si planeas desplegar con On Air.
Para otro enfoque sobre apps Python multipágina, consulta nuestra guía para construir un sitio web multipágina con FastHTML.
¿Qué enfoque deberías usar?
App sencilla
Usa el Enfoque 1 (@ui.page() + frame) cuando tengas unas pocas páginas con layouts diferentes, o quieras que cada página sea una instancia independiente. Bueno para sitios públicos.
Dashboard / herramienta interna
Usa el Enfoque 2 (ui.sub_pages), la opción recomendada por defecto para la mayoría de los lectores. Navegación estilo SPA, shell persistente, transiciones rápidas del lado del cliente. Úsalo para dashboards, paneles de administración y herramientas internas.
App grande, muchas funcionalidades
Usa el Enfoque 3 (APIRouter) cuando tengas muchos grupos de funcionalidades, necesites prefijos de URL y quieras mantener los módulos limpiamente separados. Escala bien para desarrollo en equipo.
Verificación y solución de problemas de tus páginas NiceGUI
Cómo verificar que todas las rutas están registradas
Después de arrancar la app:
python main.pyComprueba lo siguiente:
- Abre
http://localhost:8080. La página de inicio debería cargar con el header y la navegación. - Haz clic en cada enlace del menú. Cada página debería renderizar su contenido dentro del frame compartido.
- Revisa la terminal por errores de importación o advertencias
- Confirma la versión de NiceGUI:
pip show nicegui(debería ser ≥ 3.0.0)
Errores comunes y soluciones
RuntimeError: 'ui.page cannot be used in the global scope'
Mueve toda la creación de UI dentro de funciones @ui.page(). NiceGUI v3 eliminó el cliente compartido auto-index. Los elementos de UI en el ámbito global ya no están permitidos. Si tienes ui.label('something') en el nivel superior de un archivo, envuélvelo en una función de página.
ImportError en el módulo theme o menu
Ejecuta python main.py desde el directorio raíz del proyecto (donde viven theme.py y menu.py). Asegúrate de que pages/__init__.py existe (un archivo vacío es suficiente). Si usas una estructura anidada, revisa tu PYTHONPATH.
Las páginas muestran una página de error de 'cara triste'
Revisa la terminal para ver el traceback de Python. La función de página lanzó una excepción. Causas comunes: un import que falta, un error tipográfico en el nombre de un elemento, o referenciar una variable que no existe en el ámbito de la página.
Los enlaces de navegación no funcionan / 404
Verifica que las rutas coincidan exactamente, incluidas las barras finales. /youtube-title-generator/ no es lo mismo que /youtube-title-generator. Las rutas en ui.link() deben coincidir con las rutas en @ui.page() o ui.sub_pages().
response_timeout exceeded
Los constructores de página asíncronos que tardan más de 3 segundos necesitan un timeout mayor: @ui.page('/', response_timeout=10). Esto ocurre cuando las páginas hacen cómputo pesado o llamadas de red durante la inicialización.
El layout se ve mal tras actualizar desde v1/v2
NiceGUI v3 incluye Tailwind 4 (actualizado desde Tailwind 3). Algunas clases de utilidad se comportan de forma diferente, en particular line-height, los valores por defecto de border y el espaciado. Consulta el changelog de Tailwind 4 si tu layout cambió tras la actualización.
Desplegando tu app NiceGUI multipágina
Docker y proxy inverso
NiceGUI se ejecuta como un único worker de uvicorn: un proceso gestiona todas las conexiones de cliente. En producción, ponlo detrás de un proxy inverso (Nginx, Caddy o Traefik) y usa Docker para despliegues reproducibles.
La imagen oficial de Docker de NiceGUI es zauberzeug/nicegui. Para una guía completa sobre contenedorizar apps Python, consulta desplegar apps Python con Docker.
Ejemplo rápido de Docker Compose:
services:
nicegui:
image: zauberzeug/nicegui:latest
ports:
- "8080:8080"
volumes:
- .:/app
environment:
- PORT=8080
- HOST=0.0.0.0NiceGUI v3.10+ lee correctamente client.ip detrás de proxies inversos. Asegúrate de que tu proxy envíe las cabeceras X-Forwarded-For y X-Forwarded-Proto.
Notas de producción
Cosas clave que debes saber antes de ejecutar esto en producción:
- Proceso worker único: Todos los clientes comparten un proceso. Cada conexión
@ui.page()mantiene su propio árbol de UI en memoria. Para apps de alto tráfico, escala horizontalmente con réplicas de Docker detrás de un balanceador de carga. - Estado compartido: Cada cliente obtiene su propia instancia de UI. Usa
app.storagepara el estado por usuario o la claseEvent(v3.0+) para comunicación entre clientes. - Autoalojamiento: Para un VPS asequible, Hetzner Cloud ofrece planes desde ~4 €/mes. Hostinger VPS es otra opción económica con virtualización KVM y almacenamiento NVMe.
- Plataformas de despliegue: Puedes autoalojar con Dokploy o consultar los mejores paneles de servidor autoalojados para más opciones.
Los precios de los VPS subieron en todos los proveedores en 2026 — si estás replanteándote una máquina alquilada, mira qué cambió y cuándo gana un mini PC.
Consejos extra para NiceGUI v3
Algunas cosas que vale la pena saber y que no encajan en las secciones anteriores:
- Navegación programática: Usa
ui.navigate.to('/new-page')— el antiguoui.open()se eliminó en v3. - Props observables:
.props(),.classes()y.style()ya no necesitan llamadas a.update(). Son reactivos automáticamente en v3. - Tailwind 4: NiceGUI v3 incluye Tailwind 4. Si tus clases se ven raras tras actualizar, consulta el changelog de Tailwind 4 por cambios importantes en line-height y valores por defecto de border.
- Extensión de NiceGUI para VSCode: Recomendada para el autocompletado de clases Tailwind. Reemplaza la API de Python
.tailwindque se eliminó. ui.keep_alive(): Evita desconexiones de clientes inactivos (añadido en v3.11). Útil para sesiones de dashboard de larga duración.
Checklist rápido de migración a v3
- Reemplaza
ui.open()porui.navigate.to() - Elimina el uso de la API
.tailwind— usa.classes()en su lugar - Elimina las llamadas a
.update()después de.props()/.classes()/.style() - Asegura Python 3.10 o más reciente
- Fija
nicegui>=3.0.0en requirements - Mueve toda la UI dentro de funciones
@ui.page()(sin elementos en el ámbito global)
Conclusión
Añadir múltiples páginas a NiceGUI se reduce a elegir el patrón adecuado para tu app:
@ui.page()+ frame compartido para sitios multipágina sencillos con layouts distintosui.sub_pagespara dashboards y herramientas internas con navegación consistente — esta es la opción recomendada por defecto para la mayoría de los proyectos nuevosAPIRouterpara apps grandes con muchos grupos de funcionalidades y desarrollo en equipo
El enfoque de ui.sub_pages es lo primero que usaría en cualquier proyecto nuevo de NiceGUI. La navegación del lado del cliente se siente más rápida, el shell persistente evita el parpadeo del layout, y el código es más simple — todo en un solo manejador @ui.page('/') con un diccionario de rutas a funciones.
Si estás construyendo herramientas con IA sobre NiceGUI, echa un vistazo a nuestra guía sobre crear herramientas de IA con Python. Para el contexto más amplio del ecosistema, consulta el resumen de los mejores frameworks web de Python.
Documentación oficial de NiceGUIPreguntas frecuentes
¿Puedo usar NiceGUI con Tailwind CSS?
Sí. NiceGUI v3 incluye Tailwind 4. Usa .classes('your-tailwind-classes') en cualquier elemento. La antigua API de Python .tailwind (p. ej., ui.element.tailwind.font_bold()) se eliminó en v3 — usa el enfoque de .classes() con strings en su lugar.
¿Cómo comparto estado entre páginas?
En v3, cada @ui.page() crea una nueva instancia de cliente — el estado no se comparte entre usuarios por defecto. Usa app.storage para el estado persistente por usuario. Para comunicación entre clientes (p. ej., un dashboard que muestra datos de sensores en vivo a todos los usuarios), usa la clase Event introducida en v3.0.
¿Es NiceGUI adecuado para producción?
Sí, pero ejecútalo detrás de un proxy inverso (Nginx, Caddy o Traefik). NiceGUI es un proceso de worker único — un proceso uvicorn gestiona todas las conexiones de cliente. Para apps de alto tráfico, escala horizontalmente con múltiples réplicas de Docker detrás de un balanceador de carga. Cada conexión de cliente mantiene su propio árbol de UI, así que vigila el uso de memoria.
¿Cuál es la diferencia entre sub_pages y @ui.page()?
@ui.page() hace una recarga completa de página por ruta — el navegador solicita una nueva página al servidor. ui.sub_pages intercambia el contenido del lado del cliente dentro de un shell compartido (el header y la barra lateral permanecen fijos). Usa sub_pages para dashboards y apps con navegación consistente. Usa @ui.page() cuando las páginas necesiten layouts completamente diferentes o instancias de cliente independientes.


