nicegui

star 2

Build Python web UIs with NiceGUI — covering layout, widgets, data binding, routing, and the AgGrid data grid. Use this skill whenever the user mentions NiceGUI, wants to build a Python web app, dashboard, or internal tool, asks about ui.aggrid, ui.page, or any NiceGUI component. Triggers on: "nicegui", "ui.aggrid", "ui.page", "NiceGUI app", "python web ui", "python dashboard", "aggrid python", "build a web interface in python".

bmsuisse By bmsuisse schedule Updated 4/17/2026

name: nicegui plugin: coding description: > Build Python web UIs with NiceGUI — covering layout, widgets, data binding, routing, and the AgGrid data grid. Use this skill whenever the user mentions NiceGUI, wants to build a Python web app, dashboard, or internal tool, asks about ui.aggrid, ui.page, or any NiceGUI component. Triggers on: "nicegui", "ui.aggrid", "ui.page", "NiceGUI app", "python web ui", "python dashboard", "aggrid python", "build a web interface in python".

NiceGUI

NiceGUI is a Python framework that renders a reactive web UI in the browser, backed by FastAPI and Vue/Quasar. Python code runs server-side; the browser is a thin client that re-renders on state changes.

Two modes

Script mode (prototypes/simple apps) — write top-level code, executed once per connection:

from nicegui import ui
ui.label('Hello world')
ui.run()

Page mode (multi-page apps) — each decorated function is a private page per user:

from nicegui import ui

@ui.page('/')
def index():
    ui.label('Home')

@ui.page('/dashboard')
def dashboard():
    ui.label('Dashboard')

ui.run()

Auto-context

Elements are automatically added to the currently active with-context — no explicit parent parameter needed:

with ui.card():
    ui.label('Inside card')
    ui.button('Click me', on_click=lambda: ui.notify('Hi!'))

Layout elements

Element Description
ui.row() Flex row (horizontal), wrap=True by default
ui.column() Flex column (vertical)
ui.card() Card with drop shadow
ui.grid(columns=3) CSS grid
ui.tabs() / ui.tab_panels() Tab navigation
ui.splitter() Resizable split panes (.before / .after slots)
ui.expansion('Title') Accordion
ui.scroll_area() Custom scrollbar container
ui.dialog() Modal dialog
ui.header() / ui.footer() Page header/footer
ui.left_drawer() / ui.right_drawer() Side drawers
with ui.tabs().classes('w-full') as tabs:
    one = ui.tab('One')
    two = ui.tab('Two')
with ui.tab_panels(tabs, value=two).classes('w-full'):
    with ui.tab_panel(one):
        ui.label('First tab')
    with ui.tab_panel(two):
        ui.label('Second tab')

Styling

  • Tailwind CSS: .classes('text-xl font-bold text-red-500 w-full')
  • Inline CSS: .style('color: red; font-size: 20px')
  • Quasar props: .props('flat color=primary dense')

Common widgets

ui.label('text')
ui.button('Label', on_click=handler)
ui.input('Placeholder', on_change=handler)           # value via .value
ui.number('Label', min=0, max=100, value=50)
ui.checkbox('Label', on_change=handler)
ui.switch('Label')
ui.slider(min=0, max=10, step=0.5)
ui.select(['A', 'B', 'C'], label='Pick one')
ui.toggle({1: 'A', 2: 'B', 3: 'C'})
ui.date(on_change=handler)
ui.textarea('Notes')
ui.icon('home')
ui.image('https://...')
ui.markdown('**Bold** text')
ui.notify('Message', type='positive')   # type: positive|negative|warning|info

Data binding

Bind UI element properties to Python objects — updates propagate both ways automatically:

class State:
    name: str = 'Alice'

state = State()
ui.input('Name').bind_value(state, 'name')
ui.label().bind_text_from(state, 'name', backward=lambda n: f'Hello {n}')

@binding.bindable_dataclass — all fields become reactive properties (most efficient):

from nicegui import binding, ui

@binding.bindable_dataclass
class State:
    count: int = 0

s = State()
ui.number().bind_value(s, 'count')
ui.label().bind_text_from(s, 'count', backward=lambda v: f'Count: {v}')

bind_value — two-way binding
bind_value_from / bind_value_to — one-way
bind_visibility_from(obj, 'flag') — show/hide element

Bind to app.storage.user for persistence across page loads:

from nicegui import app, ui
ui.textarea().bind_value(app.storage.user, 'note')

Events

ui.button('Save', on_click=lambda: save())

# Async handler — use for awaitable grid methods etc.
async def handle():
    rows = await grid.get_selected_rows()
    ui.notify(str(rows))

ui.button('Selected', on_click=handle)

# Any AG Grid or DOM event
grid.on('cellClicked', lambda e: ui.notify(e.args['value']))

Timers

ui.timer(1.0, callback, once=False)   # recurring; once=True fires once

Navigation

ui.navigate.to('/other')
ui.navigate.to('https://example.com')
ui.navigate.back()
ui.navigate.reload()

Custom FastAPI integration

from nicegui import app
@app.get('/api/data')
def get_data():
    return {'value': 42}

ui.aggrid — AG Grid

For detailed reference see references/aggrid.md. Key points:

grid = ui.aggrid({
    'columnDefs': [
        {'headerName': 'Name', 'field': 'name'},
        {'headerName': 'Age', 'field': 'age'},
    ],
    'rowData': [
        {'name': 'Alice', 'age': 18},
        {'name': 'Bob', 'age': 21},
    ],
    'rowSelection': {'mode': 'multiRow'},  # or 'singleRow'
})

Update data — mutate grid.options['rowData'] and call grid.update():

grid.options['rowData'].append({'name': 'Carol', 'age': 42})
grid.update()

Selection (async):

async def show_selected():
    rows = await grid.get_selected_rows()    # list[dict]
    row  = await grid.get_selected_row()     # dict | None (first selected)

Run AG Grid API methods:

grid.run_grid_method('selectAll')
grid.run_grid_method('setColumnsVisible', ['parent'], True)
await grid.run_grid_method('getSelectedRows')  # await to get return value

Update a single row (preserves selection):

# requires ':getRowId' option
grid = ui.aggrid({
    ...,
    ':getRowId': '(params) => params.data.name',
})
grid.run_row_method('Alice', 'setDataValue', 'age', 99)

From DataFrame:

import pandas as pd
df = pd.DataFrame({'col1': [1, 2], 'col2': [3, 4]})
ui.aggrid.from_pandas(df)

import polars as pl
df = pl.DataFrame({'col1': [1, 2]})
ui.aggrid.from_polars(df)

Filters:

{'headerName': 'Name', 'field': 'name',
 'filter': 'agTextColumnFilter', 'floatingFilter': True}

Conditional cell styling (Tailwind classes):

{'field': 'age', 'cellClassRules': {
    'bg-red-300': 'x < 21',
    'bg-green-300': 'x >= 21',
}}

Render column as HTML:

ui.aggrid({...}, html_columns=[1])  # column index 1 renders raw HTML

Themes: 'quartz' (default), 'balham', 'material', 'alpine'

ui.aggrid({...}, theme='balham')
grid.theme = 'alpine'  # change dynamically

Get/sync client edits (when 'editable': True):

data = await grid.get_client_data()          # get without changing server state
await grid.load_client_data()                # sync edits back to options['rowData']

Direct JS access:

row = await ui.run_javascript(
    f'return getElement({grid.id}).api.getDisplayedRowAtIndex(0).data'
)

Enterprise (optional):

bundle_url = f'https://cdn.jsdelivr.net/npm/ag-grid-enterprise@{ui.aggrid.VERSION}/+esm'
ui.aggrid.set_module_source(bundle_url)
ui.aggrid({...}, modules='enterprise')

Gotcha: Row data keys must not contain periods — use nested objects and dot-notation field paths ('field': 'name.first') instead.


ui.run() parameters

ui.run(
    host='0.0.0.0',
    port=8080,
    title='My App',
    dark=None,           # None=auto, True=dark, False=light
    reload=True,         # auto-reload on file changes (dev mode)
    show=True,           # open browser on start
    storage_secret='secret_key',  # required for app.storage.user
    binding_refresh_interval=0.1,
)
Install via CLI
npx skills add https://github.com/bmsuisse/skills --skill nicegui
Repository Details
star Stars 2
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator