---
title: "Tutorial de NiceGUI: Construye una App GUI en Python en 5 Minutos"
description: "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."
date: 2026-10-08
categories: ["web-development"]
tags: ["nicegui","python","python-gui"]
---

import YouTubeEmbed from "@components/widgets/YouTubeEmbed.astro";
import Notice from "@components/widgets/Notice.astro";
import ListCheck from "@components/widgets/ListCheck.astro";
import Tabs from "@components/widgets/Tabs.astro";
import Tab from "@components/widgets/Tab.astro";
import Accordion from "@components/widgets/Accordion.astro";
import Button from "@components/widgets/Button.astro";

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-nicegui/), [Streamlit vs Taipy](/streamlit-vs-taipy/), o la lista completa de [frameworks web UI para Python](/best-python-web-frameworks/).

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:

<ListCheck>
<ul>
  <li>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.</li>
  <li>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.</li>
  <li>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.</li>
  <li>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.</li>
  <li>`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.</li>
  <li>El sistema de eventos mueve datos entre elementos UI y tu código backend sin callbacks cableados a mano por todas partes.</li>
  <li>`.props()`, `.classes()` y `.style()` son observables: cámbialos y el navegador se actualiza, sin una llamada `.update()` posterior.</li>
  <li>Un framework de testing integrado maneja tu UI desde tests Python en vez de hacer clic a mano.</li>
</ul>
</ListCheck>

Esa no es la lista completa. Revisa la [documentación de NiceGUI](https://nicegui.io/documentation) 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](https://tools.bitdoze.com/) está construido con NiceGUI si quieres ver un ejemplo desplegado.

<YouTubeEmbed
  url="https://www.youtube.com/embed/tfBKRxbCsao"
  label="NiceGUI For Beginners"
/>

<Notice type="info" title="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.
</Notice>

### Instala y ejecuta NiceGUI

Lo que necesitas antes del comando pip:

<ListCheck>
<ul>
  <li>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`.</li>
  <li>pip (viene con Python) o [configura el proyecto con uv](/uv-get-start/) si quieres instalaciones más rápidas y entornos virtuales más limpios.</li>
  <li>Puerto 8080 libre en localhost (el por defecto; cambiable).</li>
  <li>Cualquier máquina Linux, macOS o Windows. Sin Node.js, sin npm.</li>
</ul>
</ListCheck>

<Tabs>
<Tab name="pip">
```bash
pip install nicegui
```
</Tab>
<Tab name="uv">
```bash
uv add nicegui
```
</Tab>
</Tabs>

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.

<Notice type="warning" title="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)`.
</Notice>

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](/nicegui-pages/).

### 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.

### Añade footer

```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](https://github.com/zauberzeug/nicegui/releases/tag/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).

<Notice type="warning" title="¿Actualizando una app en producción desde 1.x o 2.x?">
Lee las [notas del release v3.0.0](https://github.com/zauberzeug/nicegui/releases/tag/v3.0.0) antes de tocar producción. Los cambios de modo script y `sanitize` son los que más muerden.
</Notice>

<Accordion label="¿Los snippets viejos de este tutorial siguen funcionando?" group="faq" expanded="true">
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í.
</Accordion>

<Accordion label="¿Se requiere TypeScript para usar NiceGUI?" group="faq">
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.
</Accordion>

<Accordion label="¿Necesito reescribir mi app para usar @ui.page?" group="faq">
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.
</Accordion>

## 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](https://go.bitdoze.com/hetzner) 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

<Tabs>
<Tab name="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`.
</Tab>
<Tab name="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.
</Tab>
<Tab name="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.
</Tab>
</Tabs>

**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](/docker-run-python/). 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`.

<Notice type="error" title="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.
</Notice>

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

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](/streamlit-deploy-vps-cloudflare/). Si prefieres mantener todo en casa, puedes [autoalojar NiceGUI en un mini PC de servidor casero](/best-mini-pc-home-server/) 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.

<Accordion label="[Errno 98] error while attempting to bind on address ('0.0.0.0', 8080): address already in use" group="errors" expanded="true">
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)`.
</Accordion>

<Accordion label="SyntaxError o fallos de instalación en una máquina limpia" group="errors">
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](/uv-get-start/) para gestionar el Python del proyecto en vez de pelear con el paquete de la distro.
</Accordion>

<Accordion label="El navegador no se abre, o la app hace hot-reload en un servidor" group="errors">
Son los defaults dev `show=True` y `reload=True` en `ui.run()`. En un servidor usa `ui.run(show=False, reload=False)`.
</Accordion>

<Accordion label="La UI carga, y luego muestra 'connection lost' detrás de Nginx o Cloudflare" group="errors">
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](#ejecuta-nicegui-en-producción-docker--reverse-proxy) de arriba. Los túneles de Cloudflare pasan WebSockets por defecto; nginx plano no.
</Accordion>

<Accordion label="app.storage.user crashea, o las sesiones desaparecen al reiniciar" group="errors">
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.
</Accordion>

## 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](https://nicegui.io/documentation) 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](https://nicegui.io/llms.txt)). Pégala en tu agente en vez de scrapear el sitio de documentación.

<Button text="Lee la documentación de NiceGUI" link="https://nicegui.io/documentation" variant="solid" color="blue" size="md" icon="arrow-right" />