Skip to content

ModelForm — Field Types

examples/02_field_types.py · run it with uv run python examples/02_field_types.py

ModelForm — Field Types

One form showing all field types supported by NiceView:

Python type Widget
str ui.input or ui.textarea (via widget_type override)
str with password=True ui.input (password mode with toggle)
str with autocomplete=[...] ui.input (with autocomplete dropdown)
int, float ui.number
int/float with prefix/suffix ui.number (with unit decoration)
bool ui.switch or ui.checkbox
datetime.date HTML date input
datetime.time HTML time input
datetime.datetime HTML datetime-local input
datetime.timedelta ui.input (ISO 8601 duration; tolerant input: 7d, 2h30m, p1w)
Literal[...] ui.select, ui.radio, or ui.toggle (via widget_type override)
Literal[...] with with_input=True ui.select (searchable)
list[str] with options + multiple=True ui.select (multi-select)
list[Literal[...]] ui.select (multi-select; options from the Literal, no options needed)
list[Literal[...]] with props='use-chips' ui.select (multi-select, selections shown as removable chips)
list[Literal[...]] with widget_type='checkbox_group' Row/column of ui.checkbox (alternative to the multi-select)
str (color) ui.color_input (via widget_type override)
list[str] ui.input_chips
list[Annotated[str, Field(pattern=...)]] ui.input_chips (item constraints enforced by validation, not the widget)
list[int], list[float], list[bool] ui.input (comma-separated)
int with ge/le + widget_type='ui.slider' ui.slider
int with le + widget_type='ui.rating' ui.rating
list[BaseModel] Inline EditGridWrapper
pydantic.SecretStr ui.input (password with reveal button)
pydantic.Field(frozen=True) any widget, disabled
field without a default any widget, label marked *, empty value rejected

Texts come from the model, one source per destination: title becomes the label, examples[0] the placeholder, description the field's description. Where that one is shown is a rendering choice: description_as='tooltip' (the default) puts it on hover, 'hint' below the widget, None nowhere. hint and tooltip set on a niceview.Field always win over it.

This example also demonstrates how to customize the widgets, layout and style via niceview.Field metadata, ui.grid() and ElementFilter.

Source
"""
# ModelForm — Field Types

One form showing all field types supported by NiceView:

| Python type | Widget |
|---|---|
| `str` | `ui.input` or `ui.textarea` (via `widget_type` override) |
| `str` with `password=True` | `ui.input` (password mode with toggle) |
| `str` with `autocomplete=[...]` | `ui.input` (with autocomplete dropdown) |
| `int`, `float` | `ui.number` |
| `int`/`float` with `prefix`/`suffix` | `ui.number` (with unit decoration) |
| `bool` | `ui.switch` or `ui.checkbox` |
| `datetime.date` | HTML date input |
| `datetime.time` | HTML time input |
| `datetime.datetime` | HTML datetime-local input |
| `datetime.timedelta` | `ui.input` (ISO 8601 duration; tolerant input: `7d`, `2h30m`, `p1w`) |
| `Literal[...]` | `ui.select`, `ui.radio`, or `ui.toggle` (via `widget_type` override) |
| `Literal[...]` with `with_input=True` | `ui.select` (searchable) |
| `list[str]` with `options` + `multiple=True` | `ui.select` (multi-select) |
| `list[Literal[...]]` | `ui.select` (multi-select; options from the `Literal`, no `options` needed) |
| `list[Literal[...]]` with `props='use-chips'` | `ui.select` (multi-select, selections shown as removable chips) |
| `list[Literal[...]]` with `widget_type='checkbox_group'` | Row/column of `ui.checkbox` (alternative to the multi-select) |
| `str` (color) | `ui.color_input` (via `widget_type` override) |
| `list[str]` | `ui.input_chips` |
| `list[Annotated[str, Field(pattern=...)]]` | `ui.input_chips` (item constraints enforced by validation, not the widget) |
| `list[int]`, `list[float]`, `list[bool]` | `ui.input` (comma-separated) |
| `int` with `ge`/`le` + `widget_type='ui.slider'` | `ui.slider` |
| `int` with `le` + `widget_type='ui.rating'` | `ui.rating` |
| `list[BaseModel]` | Inline `EditGridWrapper` |
| `pydantic.SecretStr` | `ui.input` (password with reveal button) |
| `pydantic.Field(frozen=True)` | any widget, disabled |
| field without a default | any widget, label marked `*`, empty value rejected |

Texts come from the model, one source per destination: `title` becomes the label,
`examples[0]` the placeholder, `description` the field's `description`. Where that one is shown
is a rendering choice: `description_as='tooltip'` (the default) puts it on hover, `'hint'` below
the widget, `None` nowhere. `hint` and `tooltip` set on a `niceview.Field` always win over it.

This example also demonstrates how to customize the widgets, layout and style via `niceview.Field` metadata, ui.grid() and `ElementFilter`.
"""

import datetime
from typing import Annotated, Literal
import pydantic
from nicegui import ElementFilter, ui

import niceview
from niceview import ModelForm


class Tag(pydantic.BaseModel):
    label: str = pydantic.Field(default='', title='Label')

    def __str__(self):
        return self.label


class AllTypes(pydantic.BaseModel):
    # required (no default): the label gets a ' *' marker and an empty value is rejected,
    # with or without pydantic — see docs/components.md#validation
    handle: Annotated[str, pydantic.Field(title='Handle (required)', description='Unique, lowercase', examples=['a-handle'])]
    text: str = pydantic.Field(default='hello', title='String')
    text_area: Annotated[str, niceview.Field(widget_type='ui.textarea', label='String in Textarea')] = 'hello\nworld'
    password: Annotated[str, pydantic.Field(title='Password'), niceview.Field(password=True, password_toggle_button=True)] = 'hunter2'
    city: Annotated[str, pydantic.Field(title='City (autocomplete)'), niceview.Field(autocomplete=['Berlin', 'Munich', 'Hamburg', 'Cologne', 'Frankfurt', 'Stuttgart'])] = 'Berlin'
    number_int: int = pydantic.Field(default=42, ge=0, le=1000, title='Integer (0-1000)')
    number_float: float = pydantic.Field(default=3.14, title='Float')
    speed: Annotated[float, pydantic.Field(title='Speed'), niceview.Field(suffix=' km/h', precision=1)] = 120.0
    flag_switch: bool = pydantic.Field(default=True, title='Bool')
    flag_checkbox: Annotated[bool, pydantic.Field(title='Bool in Checkbox'), niceview.Field(widget_type='ui.checkbox')] = False
    date: datetime.date = pydantic.Field(default_factory=datetime.date.today, title='Date')
    time: datetime.time = pydantic.Field(default_factory=lambda: datetime.time(9, 0), title='Time')
    dt: datetime.datetime = pydantic.Field(
        default_factory=lambda: datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0),
        title='Datetime',
    )
    duration: Annotated[datetime.timedelta, niceview.Field(
        hint='ISO 8601 (P7D, PT1H30M) or shorthand (7d, 2h30m) — normalised on blur',
    )] = pydantic.Field(
        default_factory=lambda: datetime.timedelta(hours=1, minutes=30),
        title='Timedelta',
    )
    choice: Literal['red', 'green', 'blue'] = 'green' # label and widget are auto-detected from the Literal type
    choice_search: Annotated[Literal['apple', 'banana', 'cherry', 'date', 'elderberry'], niceview.Field(widget_type='ui.select', with_input=True, label='Fruit (searchable select)')] = 'apple'
    choice_multi: Annotated[list[str], niceview.Field(widget_type='ui.select', options=['red', 'green', 'blue'], multiple=True, clearable=True)] = pydantic.Field(default_factory=lambda: ['red', 'blue'], title='Colors (multi-select)')
    perms_multiselect: list[Literal['read', 'write', 'admin']] = pydantic.Field(default_factory=lambda: ['read'], title='Permissions (list[Literal], auto multi-select)')  # type: ignore[arg-type]
    perms_chips: Annotated[list[Literal['read', 'write', 'admin']], niceview.Field(props='use-chips', label='Permissions (multi-select with use-chips)')] = pydantic.Field(default_factory=lambda: ['read'])  # type: ignore[arg-type]
    perms_checkboxes: Annotated[list[Literal['read', 'write', 'admin']], niceview.Field(widget_type='checkbox_group', props='inline', label='Permissions (checkbox_group)')] = pydantic.Field(default_factory=lambda: ['read'])  # type: ignore[arg-type]
    choice_radio: Annotated[Literal['red', 'green', 'blue'], niceview.Field(widget_type='ui.radio', props='inline')] = 'green'
    choice_toggle: Annotated[Literal['red', 'green', 'blue'], niceview.Field(widget_type='ui.toggle')] = 'green'
    color: Annotated[str, niceview.Field(widget_type='ui.color_input', label='Color', color_preview=True)] = '#4a90e2'
    volume: Annotated[int, pydantic.Field(ge=0, le=100, title='Volume'), niceview.Field(widget_type='ui.slider', step=1)] = 50
    priority: Annotated[int, pydantic.Field(ge=1, le=5, title='Priority'), niceview.Field(widget_type='ui.rating')] = 3
    chips: list[str] = pydantic.Field(default_factory=lambda: ['foo', 'bar'], title='Chips (list[str])')
    chips_constrained: list[Annotated[str, pydantic.Field(pattern=r'^[a-z]+$', min_length=2, max_length=10)]] = pydantic.Field(
        default_factory=lambda: ['ok', 'go'],
        title='Chips (constrained items: lowercase, 2-10 chars)',
    )
    nums: list[int] = pydantic.Field(default_factory=lambda: [1, 2, 3], title='Numbers (list[int], comma-separated)')
    secret: pydantic.SecretStr = pydantic.Field(default=pydantic.SecretStr('s3cret'), title='SecretStr (password input)')
    created: str = pydantic.Field(default='2026-08-06', title='Frozen (disabled)', frozen=True)
    tags: list[Tag] = pydantic.Field(
        default_factory=lambda: [Tag(label='important')],
        title='Tags (list of BaseModel with __str__ method)',
    )

@ui.page('/')
def page():
    # Global styling alternative (applies to every ui.input in the app):
    #   ui.input.default_props('outlined dense')
    # The scoped alternative via ElementFilter is shown at the end of this function.

    with ui.tabs().classes('w-full') as tabs:
        tab_home = ui.tab('Documentation')
        tab_all_types = ui.tab('All Types')

    with ui.tab_panels(tabs, value=tab_home).classes('w-full'):

        with ui.tab_panel(tab_home):
            ui.markdown(__doc__ or '')

        with ui.tab_panel(tab_all_types):
            with ui.grid().classes('w-full gap-4 grid-cols-1 lg:grid-cols-2').mark('my-form'):
                ModelForm.from_item(AllTypes(handle='alice')).render()

    # Styling example: make all my-form elements outlined and dense
    ElementFilter().within(marker='my-form').props('outlined dense')

ui.run(title='02 — Field Types')