Logotipo de Bitdoze

Tutorial de NiceGUI: Construye una App GUI en Python en 5 Minutos

Tutorial de NiceGUI para principiantes: instala NiceGUI, construye tu primera GUI en Python con labels, filas, inputs y botones, y ejecútala en producción. Actualizado para 3.x.

Dragos

Actualizado Publicado 36 min de lectura

Tutorial de NiceGUI: Construye una App GUI en Python en 5 Minutos

NiceGUI es un framework Python para construir GUIs web. Vue y Quasar renderizan el frontend sobre FastAPI, Tailwind CSS se encarga del estilo, y todo lo que escribes se queda en Python. Este tutorial de NiceGUI te deja una GUI en Python funcionando en unos cinco minutos, y luego cubre qué cambió en 3.x y cómo ejecutar la app en producción.

A octubre de 2026 NiceGUI está en la versión 3.18.0 (publicada el 2026-10-07), con 16.3k estrellas en GitHub, unas 890k descargas mensuales en PyPI, y una licencia MIT mantenida por Zauberzeug. Requiere Python 3.10 o más nuevo.

Con el tiempo probé varios frameworks Python como Streamlit y Taipy antes de decidirme por NiceGUI. Streamlit es más rápido para un dashboard desechable; NiceGUI gana cuando quiero control sobre el layout, el estilo y la velocidad. Si aún estás eligiendo, mira la comparación Streamlit vs. NiceGUI, Streamlit vs Taipy, o la lista completa de frameworks web UI para Python.

Cada snippet de abajo corre en NiceGUI 3.18.0. Ese es el punto de un tutorial de NiceGUI: instálalo, copia el código, verifica que sirve en el puerto 8080.

Funciones de NiceGUI

Estas son las partes que importan al construir una GUI en Python con NiceGUI, y las que sigo usando en apps reales:

  • El rendimiento aguanta porque la UI habla con tu código Python por WebSocket, así que las interacciones se sienten instantáneas. Los visitantes no adivinarán que hay Python detrás.
  • La personalización llega lejos: clases de Tailwind CSS (Tailwind 4 desde 3.0, UnoCSS opcional desde 3.7), props de Quasar y CSS crudo cubren casi cualquier diseño. También puedes meter JavaScript cuando lo necesites.
  • En el día a día escribes Python y nada más. La documentación y la galería de ejemplos cubren la mayoría de patrones.
  • Los componentes cubren los casos comunes: row, column, markdown, image, slider, card, table, aggrid, editor, upload y muchos más, todos vinculables a variables Python normales.
  • ui.sub_pages con el parámetro root (desde 3.0) construye una single-page app con navegación del lado cliente en un solo archivo, sin boilerplate de rutas.
  • El sistema de eventos mueve datos entre elementos UI y tu código backend sin callbacks cableados a mano por todas partes.
  • .props(), .classes() y .style() son observables: cámbialos y el navegador se actualiza, sin una llamada .update() posterior.
  • Un framework de testing integrado maneja tu UI desde tests Python en vez de hacer clic a mano.

Esa no es la lista completa. Revisa la documentación de NiceGUI para la referencia de componentes.

Empezando con NiceGUI

En las siguientes secciones instalas NiceGUI, ejecutas una primera página, y recorres los componentes y personalizaciones usados en la mayoría de apps. Bitdoze Tools está construido con NiceGUI si quieres ver un ejemplo desplegado.

Vídeo vs. versión actual

El vídeo se grabó en marzo de 2024 contra la UI de la era 1.x. Cada snippet de abajo corre en NiceGUI 3.18.0 (octubre 2026), así que puedes seguirlo con el paquete actual.

Instala y ejecuta NiceGUI

Lo que necesitas antes del comando pip:

  • Python 3.10 o más nuevo. NiceGUI 3.0 soltó Python 3.8 y el paquete ahora requiere 3.10+. Comprueba con python3 --version.
  • pip (viene con Python) o configura el proyecto con uv si quieres instalaciones más rápidas y entornos virtuales más limpios.
  • Puerto 8080 libre en localhost (el por defecto; cambiable).
  • Cualquier máquina Linux, macOS o Windows. Sin Node.js, sin npm.

pip

bash
pip install nicegui

uv

bash
uv add nicegui

Crea un archivo llamado main.py:

python
from nicegui import ui

ui.label('Hello NiceGUI!')

ui.run()

La anatomía es siempre la misma:

  • from nicegui import ui importa el módulo, como la mayoría de frameworks UI.
  • El medio es tu UI. ui.label('Hello NiceGUI!') pone texto en la página; todo lo demás de abajo son variaciones sobre esta línea.
  • ui.run() arranca el servidor.

Ejecútalo:

bash
python3 main.py

Verifica: NiceGUI imprime la URL y sirve en http://localhost:8080. En un terminal puedes confirmar con:

bash
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080

Quieres 200. Una respuesta vacía o Connection refused significa que el proceso murió o el puerto difiere; lee la salida del terminal.

ui.run() viene con valores por defecto de desarrollo

ui.run() corre con reload=True (reconstruye la UI al guardar el archivo) y show=True (abre tu navegador). Eso es lo que quieres en un portátil. En un servidor ambos están mal: apágalos con ui.run(reload=False, show=False).

Un matiz de 3.x que vale conocer ahora: el código en el scope global como el ejemplo de arriba es un “script NiceGUI”. Funciona, pero la UI se recrea en cada visita a la página y no puede compartirse entre clientes. Cuando te quedes corto (headers compartidos, múltiples URLs), pasas a funciones @ui.page o ui.run(root=...). Eso es lo que cubre la guía de añadir múltiples páginas a NiceGUI.

Añade Markdown

El texto formateado pasa por ui.markdown:

python
ui.markdown('# This is H1 Header')
ui.markdown('## This is H2 Header')
ui.markdown('### This is H3 Header')

Los encabezados markdown mapean directo a headings HTML: un # es un H1, dos son un H2, tres son un H3.

Añade filas

python
# row element
with ui.row():
  ui.label(' First row item')
  ui.label(' Second row item')
  ui.label(' Third row item')

Esto crea un layout en fila: los labels quedan lado a lado, horizontalmente. Mete los elementos que necesites dentro del bloque with ui.row():.

Añade columnas

python
# Column element
with ui.column():
  ui.label(' First column item')
  ui.label(' Second column item')
  ui.label(' Third column item')

La misma idea, apilado verticalmente. ui.column() es el contenedor por defecto en la mayoría de mis páginas; ui.row() cuando necesito cosas una al lado de la otra.

Estilizando componentes con clases Tailwind

python
with ui.column():
  ui.label(' First column item').classes('font-bold')
  ui.label(' Second column item').classes('text-2xl')
  ui.label(' Third column item').classes('text-red-600 text-2xl')

.classes() acepta cualquier clase utilitaria de Tailwind. NiceGUI 3.x trae Tailwind CSS 4, y las utilidades comunes (font-bold, text-2xl, text-red-600, shadow-lg, rounded-lg) se comportan igual que antes. En 3.x estas clases también son observables: cambiarlas actualiza el navegador inmediatamente, sin una llamada .update().

Inputs y botones (tu primera interacción)

python
with ui.row():
    ui.input(label='Type Something').props('square outlined dense').classes('shadow-lg')
    ui.button('Click Me')

Esto añade un input con label y un botón en una fila. .props() pone props de Quasar (square outlined dense es un trío de props de Quasar), mientras .classes() añade clases Tailwind. Mezclar ambos es el flujo de estilo normal.

Los elementos estáticos están bien, pero el punto de una GUI en Python es llamar a Python. Aquí está el ejemplo on_click de botón NiceGUI útil más pequeño:

python
from nicegui import ui

with ui.card():
    name = ui.input(label='Your name')
    ui.button('Greet', on_click=lambda: ui.notify(f'Hello, {name.value}!'))

ui.run()

Esto es lo que pasa: on_click registra un callback Python para el botón. name.value lee lo que haya en el input en el momento del clic. ui.notify(...) muestra un pequeño mensaje toast en la página. Ejecútalo, escribe tu nombre, haz clic en Greet. Esa es una app interactiva funcionando en siete líneas.

Añade imágenes

python
ui.image('https://picsum.photos/id/24/600/400').classes('h-auto max-w-lg rounded-lg flex justify-center')

ui.image toma una URL (o una ruta de archivo local en tu proyecto) y aplican las clases Tailwind de siempre: alto automático, ancho limitado, esquinas redondeadas, centrado en la página. Prefiere un asset local o una URL estable en apps reales; los artefactos de build con hash se mueven al rebuildear el sitio.

Añade header con toggle de drawer

python
with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
        ui.label('HEADER')
        ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')

Esto crea una barra de header coloreada con un label de título y un botón hamburguesa que alterna un drawer del lado derecho. Fíjate que el botón referencia right_drawer antes de estar definido en el archivo; eso funciona porque el lambda corre después, en el momento del clic.

Añade sidebar/drawer derecho

python
with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as right_drawer:
        ui.label('RIGHT DRAWER')

La parte as right_drawer es sobre la que el botón del header llama .toggle(). Mantén el bloque del header antes que este en el archivo para que la variable se lea natural.

python
with ui.footer().style('background-color: #3874c8'):
        ui.label('FOOTER')

Un footer anclado al fondo de la página. Divídelo en secciones ui.row() o columnas si necesitas grupos de enlaces.

Añade código personalizado al header

python
ui.add_head_html('''
    <script></script>
    ''')

ui.add_head_html inyecta HTML crudo en el <head> de la página. Así es como añades JavaScript, CSS personalizado o un snippet de analítica.

El archivo de ejemplo completo

Juntándolo todo, el archivo queda así:

python
from nicegui import ui

# demonstrate h1 and others
ui.markdown('# This is H1 Header')
ui.markdown('## This is H2 Header')
ui.markdown('### This is H3 Header')

ui.separator()

# row element
with ui.row():
  ui.label(' First row item')
  ui.label(' Second row item')
  ui.label(' Third row item')


ui.separator()
# Column element
with ui.column():
  ui.label(' First column item')
  ui.label(' Second column item')
  ui.label(' Third column item')

# style the text
ui.separator()

with ui.column():
  ui.label(' First column item').classes('font-bold')
  ui.label(' Second column item').classes('text-2xl')
  ui.label(' Third column item').classes('text-red-600 text-2xl')

# other elements
ui.separator()
with ui.row():
    ui.input(label='Type Something').props('square outlined dense').classes('shadow-lg')
    ui.button('Click Me')


# image add
ui.separator()
ui.image('https://picsum.photos/id/24/600/400').classes('h-auto max-w-lg rounded-lg flex justify-center')


# header with right drawer
with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
        ui.label('HEADER')
        ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')
with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered') as right_drawer:
        ui.label('RIGHT DRAWER')

# footer
with ui.footer().style('background-color: #3874c8'):
        ui.label('FOOTER')


ui.run()  # dev: http://localhost:8080, auto-reload, opens your browser
# server/prod: ui.run(host='0.0.0.0', port=8080, reload=False, show=False, storage_secret='change-me')

Verifica: python3 main.py, luego abre http://localhost:8080. Deberías ver los encabezados markdown, los elementos de fila y columna, el texto rojo en negrita, el input y el botón, la imagen, el header azul con un botón de menú, y el footer. Haz clic en el botón de menú: el drawer derecho se desliza.

Esto araña la superficie de lo que NiceGUI puede hacer, pero es suficiente para entender los bloques de construcción. Lo siguiente: conecta los botones e inputs a tus funciones Python (mira el ejemplo on_click de arriba), y luego echa un vistazo al setup de producción más abajo.

NiceGUI 3.x: qué cambió desde 2.x

Si la última vez que tocaste NiceGUI fue cuando se publicó este artículo por primera vez, estás en código de la era 1.x. El historial de versiones es así:

  • 1.4.x en marzo de 2024 (cuando salió este artículo)
  • 2.0.0 el 2024-08-31, cuando el proyecto adoptó versionado semántico
  • 3.0.0 el 2025-10-03, con los breaking changes listados abajo
  • 3.18.0 el 2026-10-07, la versión actual

Las partes relevantes para principiantes del release v3.0.0:

  • La página auto-index compartida desapareció. Ahora eliges uno de tres modelos: un script plano (scope global, como este tutorial), ui.run(root=my_function) para una single-page app, o funciones @ui.page en todas partes. No mezcles UI de scope global con @ui.page en el mismo archivo.
  • .props(), .classes() y .style() son observables. Borra las llamadas .update() posteriores.
  • ui.table, ui.aggrid, ui.echart, ui.json_editor, ui.tree: muta table.rows / aggrid.options directamente en vez de reconstruir las listas Python originales.
  • Tailwind CSS 4 reemplazó a Tailwind 3.4. La API Python element.tailwind fue eliminada. Las clases utilitarias estándar siguen funcionando.
  • Se soltó el soporte de Python 3.8. Hoy el paquete requiere Python 3.10+.
  • ui.html y ui.chat_message(text_html=True) ahora toman un argumento sanitize explícito.
  • Los handlers de eventos de upload reciben un único objeto e.file (.name, .content_type, .read(), .save()) en vez de los args separados content/name/type.
  • ui.open se convirtió en ui.navigate.to.

Vale explorar una vez que lo básico funciona: ui.sub_pages para navegación SPA, make_sortable() para drag and drop (3.11), ui.popup (3.15), ui.parallax (3.9), theming con app.colors (3.6), y ui.on_exception (3.6).

¿Actualizando una app en producción desde 1.x o 2.x?

Lee las notas del release v3.0.0 antes de tocar producción. Los cambios de modo script y sanitize son los que más muerden.

¿Los snippets viejos de este tutorial siguen funcionando?

Sí. El archivo de ejemplo combinado de arriba corre en NiceGUI 3.18.0 (octubre 2026). Los breaking changes de 3.x afectan la arquitectura de la app (modelo de páginas), el data binding de tablas, y un puñado de argumentos relacionados con seguridad, no la API de elementos usada aquí.

¿Se requiere TypeScript para usar NiceGUI?

No. NiceGUI es un framework Python. Esa vieja afirmación de “soporte TypeScript mejorado” que flota por ahí estaba mal; escribes Python, y el frontend Vue/Quasar se genera por ti. Solo tocas JavaScript cuando lo inyectas explícitamente.

¿Necesito reescribir mi app para usar @ui.page?

No si tu app actual es un único script de scope global y eso te sirve. Pasa a @ui.page o ui.run(root=...) cuando necesites múltiples URLs, un layout compartido entre visitantes, o estado por usuario que sobreviva a la navegación. La guía multi-página enlazada en la sección Empezando cubre los tres modelos.

Ejecuta NiceGUI en producción (Docker + reverse proxy)

Una vez que la app funciona localmente, necesitas un sitio donde correrla. Para la mayoría de herramientas internas y apps públicas pequeñas un VPS de 4-5 EUR/mes de Hetzner Cloud tiene CPU y RAM suficientes. La app de ejemplo de este tutorial es un proceso Python pequeño, así que hasta el tier más barato tiene margen.

NiceGUI corre como un único worker uvicorn con I/O asíncrono completo. No lo escales añadiendo workers. Para trabajo CPU-bound usa run.cpu_bound(...) para empujar jobs a un process pool en su lugar.

Flags de ui.run() en producción

main.py prod

python
import os
from nicegui import ui

ui.label('Hello from production')

ui.run(
    host='0.0.0.0',
    port=8080,
    reload=False,
    show=False,
    storage_secret=os.environ['STORAGE_SECRET'],
)

storage_secret es requerido si usas app.storage.user o app.storage.browser; sin él esas sesiones no persisten. En un servidor también pon reload=False y show=False.

docker-compose.yml

La imagen oficial zauberzeug/nicegui (unos 100k pulls en Docker Hub) espera tu código en /app y lo corre como un usuario no-root fijado por PUID/PGID:

yaml
services:
  nicegui:
    image: zauberzeug/nicegui:latest
    container_name: nicegui_app
    restart: unless-stopped
    ports:
      - "8080:8080"
    volumes:
      - ./app:/app
      - nicegui_storage:/app/.nicegui
    environment:
      - PUID=1000
      - PGID=1000
      - STORAGE_SECRET=change-me-to-a-long-random-string

volumes:
  nicegui_storage:

Pon el main.py de producción de la primera pestaña en ./app/main.py. El volumen nicegui_storage mantiene los datos de sesión .nicegui entre reinicios.

Nginx

NiceGUI habla con el navegador por WebSocket. Un reverse proxy sin headers de upgrade lo rompe (síntoma: “connection lost” tras cargar la página). Config mínima de nginx:

nginx
server {
    listen 443 ssl;
    server_name app.example.com;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "Upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

NiceGUI 3.14.0 también endureció la reflexión de X-Forwarded-Prefix, así que mantén la imagen al día si sirves bajo un prefijo de ruta.

Verifica tras docker compose up -d:

bash
docker compose logs -f nicegui   # uvicorn startup line, no tracebacks
curl -I http://localhost:8080    # HTTP/1.1 200 OK

A través del proxy, abre las devtools del navegador y confirma que la petición WebSocket devuelve 101 Switching Protocols. Si devuelve 404 o se cuelga, tu proxy no está reenviando los headers de upgrade.

Si quieres los patrones generales para contenerizar apps Python, mira cómo correr cualquier app Python en Docker con Docker Compose. El mismo layout de compose funciona para FastHTML, Streamlit o un servicio FastAPI plano.

Rollback es un comando: docker compose down. Añade -v solo cuando pretendas borrar también el volumen de almacenamiento .nicegui.

No expongas un NiceGUI desactualizado a internet

NiceGUI parcheó un goteo constante de CVEs durante 2025 y 2026: XSS vía ui.markdown (GHSA-v82v-c5x8-w282, arreglado en 3.7.0), path traversal en FileUpload.name (3.7.0), XSS vía run_method() (3.8.0), agotamiento de memoria vía media streaming (3.9.0) y clientes filtrados (3.16.0), y rutas de descarga inadivinables (3.17.0). Corre pip install -U nicegui o descarga zauberzeug/nicegui:latest antes de desplegar, y sigue actualizando después.

  • reload=False y show=False en ui.run()
  • storage_secret fijado desde una variable de entorno, no hardcodeado
  • El contenedor corre como no-root (PUID/PGID o una directiva user:)
  • El directorio de almacenamiento .nicegui persistido en un volumen
  • Headers de upgrade WebSocket configurados en el reverse proxy
  • Imagen fijada o actualizada en un horario, no dejada en un tag de 2025

Una opción solo para demos: ui.run(on_air=True) te da una URL pública temporal por túnel. Está etiquetada como tech preview; bien para enseñar algo a un colega, no para producción.

Para un recorrido de despliegue paralelo, incluyendo TLS vía túneles, mira cómo desplegar Streamlit en un VPS detrás de Cloudflare Tunnels. Si prefieres mantener todo en casa, puedes autoalojar NiceGUI en un mini PC de servidor casero a coste cero de hosting más la electricidad.

Errores de primera ejecución y soluciones

Los cinco fallos con más probabilidad de encontrar, con el error real primero.

[Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use

Algo más tiene el puerto 8080: otra ejecución dev de NiceGUI, un contenedor Docker olvidado (docker ps | grep 8080), o cualquier otro servicio. O paras ese proceso o mueves el tuyo: ui.run(port=8000).

SyntaxError o fallos de instalación en una máquina limpia

Casi siempre un intérprete viejo: NiceGUI 3.x requiere Python 3.10 o más nuevo (python3 --version). En Debian/Ubuntu con un Python de sistema viejo, instala un intérprete más nuevo o usa uv para gestionar el Python del proyecto en vez de pelear con el paquete de la distro.

El navegador no se abre, o la app hace hot-reload en un servidor

Son los defaults dev show=True y reload=True en ui.run(). En un servidor usa ui.run(show=False, reload=False).

La UI carga, y luego muestra 'connection lost' detrás de Nginx o Cloudflare

El reverse proxy no está reenviando los headers de upgrade WebSocket. Añade proxy_http_version 1.1, proxy_set_header Upgrade $http_upgrade, y proxy_set_header Connection "Upgrade" como en la pestaña Nginx con los headers WebSocket del reverse proxy de arriba. Los túneles de Cloudflare pasan WebSockets por defecto; nginx plano no.

app.storage.user crashea, o las sesiones desaparecen al reiniciar

Te falta storage_secret en ui.run(), o el directorio .nicegui del contenedor no está en un volumen persistente. Pon storage_secret=os.environ['STORAGE_SECRET'] y monta el volumen como en el archivo compose de arriba.

Por qué a algunos probablemente no les gustará NiceGUI

NiceGUI te da una UI Python rápida y completamente personalizable. El precio de ese control es CSS. En algún momento escribirás clases Tailwind o ajustarás props de Quasar para conseguir el look que quieres, y si nunca has tocado CSS eso se siente como un muro al principio. Streamlit es el otro camino: le tiras código y el framework toma las decisiones por ti. Para un dashboard interno rápido ese intercambio a menudo es el correcto.

NiceGUI 3.x suavizó ligeramente la curva (Tailwind 4 es más simple que 3, y ui.sub_pages quita mucho pegamento de routing), pero el trade-off no cambia: control versus magia. Si quieres una GUI Python performante que puedas moldear, NiceGUI encaja mejor.

Conclusiones

Este tutorial de NiceGUI te llevó desde pip install nicegui pasando por layout, estilo con Tailwind, un primer botón interactivo, y un despliegue en producción con Docker y un reverse proxy. El código se verificó en NiceGUI 3.18.0, vigente a octubre de 2026.

Siguientes pasos: la documentación de NiceGUI para la lista completa de componentes, y la guía multi-página cuando necesites routing. Si usas agentes de código con IA, un consejo práctico: NiceGUI trae una referencia compacta de API en nicegui/llms.md dentro del paquete pip (también en nicegui.io/llms.txt). Pégala en tu agente en vez de scrapear el sitio de documentación.

Lee la documentación de NiceGUI