Skip to content

Conversation

falkoschindler
Copy link
Contributor

@falkoschindler falkoschindler commented Sep 4, 2025

This PR collects all changes and release notes for NiceGUI 3.0.
It shouldn't get squashed.


New features and enhancements

⚠️ Breaking changes and migration guide

  1. Shared auto-index pages (not using @ui.page)

    UI elements defined in global scope have been added to a static shared "auto-index client" served at "/". This caused a multitude of problems throughout the code base, so we decided to remove this shared client.

    In version 3.0 you have the following options:

    • Keep putting (all) UI elements in global scope. We call such apps without page functions "NiceGUI scripts". They will automatically re-evaluated inside an implicit page function when visiting "/". This is almost a drop-in replacement for the auto-index client, but:

      • NiceGUI scripts can't contain page functions.
      • The UI isn't shared but re-created on every page visit.
    • Wrap all UI in a singe function and pass it to the new positional root parameter in ui.run. This is especially handy in combination with ui.sub_pages. This way you can create rich single-page applications without worrying about defining the correct routes with @page decorators.

    • Use page functions for all your pages, including the index page at "/".

    Note that we are introducing a new Event class (see below). Together with the binding module this helps to synchronize long-living objects with short-living UI without relying on a long-living shared client.

  2. Calling .props(), .classes() or .style() with subsequent .update()

    A subsequent .update() is not necessary anymore because props, classes and style are now observable collections that trigger updates automatically.

    When overwriting the update method in custom elements, infinite recursions can occur. If, e.g., the update methods uses .prop() before calling super().update(), the .prop() call will cause an infinite cycle. Wrap it with with self._props.suspend_updates(): (and similar for classes and style) to pause automatic updates in such cases.

  3. Upgrade to Tailwind 4; dropping the ui.element.tailwind API

    Although very similar, Tailwind 4 comes with some breaking changes. Check your layout carefully after upgrading. We noticed differences especially with line spacing and borders.

    For technical reasons updating and maintaining our ui.element.tailwind API became unfeasible. So we decided to remove it. For auto-completing Tailwind classes, we recommend the NiceGUI extension for Visual Studio Code by @DaelonSuzuka.

  4. Dropping support for Python 3.8

    Almost one year after Python 3.8 reached its end-of-life, it was time to drop support. This allowed us to update the code to a newer standard and resolve some issues with Python dependencies that already dropped 3.8 a while ago.

  5. ValueChangeEventArguments got a new previous_value attribute

    In some situations it might be helpful to have access to both, the current and the previous value. Therefore we added previous_value to the ValueChangeEventArguments.

    Custom elements that emit ValueChangeEventArguments need to provide the previous value.

  6. Binding from and to non-existing object properties

    Binding used to fail silently if one of the attributes doesn't exist. In case of dictionaries this was intended, because they are often used to bind to persistent storage which is empty by default. But for object properties this can lead to very subtle bugs, e.g. after renaming properties and not updating the attribue names in binding functions.

    In 3.0 object properties will be checked for existence by default. Missing dictionaries will continue to be ignored.

    You can fine-tune this behavior using the strict: bool | None = None parameter (None: check object properties and ignore dictionaries).

  7. ui.log with unspecified width can collapse inside containers with unspecified width

    We noticed that the height of ui.log could be affected by its content, which is unexpected. Therefore we decided to use a scroll area for a more robust layout.

    Now the width of ui.log can collapse when placed inside a container with an unspecified width. Either give the container some width or specify the width of ui.log.

  8. ui.clipboard.read() returns None if the clipboard API isn't available

    The read() function used to return an empty string if the clipboard API is not available. This was indistinguishable from an empty clipboard.

    Now the read() function returns None if the clipboard API is not available.

  9. Remove deprecated code and APIs (Remove deprecated code and APIs #5037 by @falkoschindler)

    • app.add_static_file and app.add_media_file raises FileNotFoundError instead of ValueError in case of non-existing files. Since FileNotFoundError is NOT a subclass of ValueError, existing code which except ValueError will fail to catch the new FileNotFoundError.
    • ui.aggrid: run_column_method is gone. Use run_grid_method instead.
    • ui.table calling add_rows()/remove_rows() with variable-length arguments no longer works. Pass a list instead or use add_row()/remove_row() for a single row.
    • ui.open is gone. Use ui.navigate.to.
    • nicegui.testing.conftest is gone and you can no longer import it. Use pytest_plugins = ["nicegui.testing.plugin"] instead.
    • element.on parameter js_handler now has type str instead of str | None. You can pass a None and it still works, but your type checker won't be happy, and we can't promise it will work for much longer afterwards.

JavaScript Dependencies

The infrastructure for managing node packages has been improved significantly (#4163, #5021 by @simontaurus, @evnchn, @falkoschindler).
The following JavaScript dependencies have been updated to the latest versions (#5034 by @falkoschindler):


Open tasks:

Solipsistmonkey and others added 22 commits August 6, 2025 10:05
I changed some instances where it had python 3.8 as Poetry 2.0.0 dropped
support for 3.8. I noticed this when trying to run "docker.sh buildup
app"

---------

Co-authored-by: Jesse Harwin <[email protected]>
Co-authored-by: Falko Schindler <[email protected]>
### Motivation

In #4925 we noticed that the height of a `ui.log` element can be
affected by its content, which is unexpected and different than, e.g.,
for `ui.scroll_area`:
```py
with ui.column().classes('w-60 items-stretch h-72'):
    ui.label('h-24').classes('border h-24')
    with ui.column().classes('h-full'):
        log = ui.log().classes('h-full')

with ui.column().classes('w-60 items-stretch h-72'):
    ui.label('h-24').classes('border h-24')
    with ui.column().classes('h-full'):
        scroll = ui.scroll_area().classes('h-full border')

def add_content():
    log.push(time.time())
    with scroll:
        ui.label(time.time())

ui.button('Log', on_click=add_content)
```

### Implementation

The problem seems to be that `ui.log` doesn't wrap its content in
multiple layers like `ui.scroll_area` does. To keep things simple, this
PR implements `ui.log` based on `<q-scroll_area>` instead of `<div>`.
This way we only need to adjust the CSS rules a little bit and now the
layout behavior of `ui.log` is identical to `ui.scroll_area`.

### Caveat

I don't think that the different underlying Vue component is a breaking
change.
**But** now `ui.log` collapses if placed inside a container with an
unspecified width:
```py
ui.log()  # ok

with ui.column():
    ui.log()  # collapses

with ui.column().classes('w-60'):
    ui.log()  # ok
```

Therefore I'm not sure if it is safe to release this PR before 3.0.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests are not necessary.
- [x] Documentation is not necessary.
### Motivation

In PR #4749 we couldn't get an ESM for ajv-formats from
registry.npmjs.org. So we decided to switch to Jsdelivr.

### Implementation

This PR re-implements parts of npm.py to work with Jsdelivr. The special
case for TailwindCSS could be removed.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests are not necessary.
- [x] Documentation is not necessary.
Inspired by discussion #4410, this PR introduces a `previous_value`
field for `ValueChangeEventArguments`. This change is pretty
straightforward and only requires adjustments for two mixins and two
elements.

Right now it is lacking backward-compatibility though. If user code
derives custom `ValueChangeEventArguments`, they are now missing the
`previous_value` field. Should we add a default value? But it should be
clear that this default is _not_ the previous value, but "not set". We
might need a sentinel...

And in 3.0 I'd like to enforce the `previous_value` field. A deprecation
warning for the default value would be great, but probably hard to
achieve.
### Motivation

There are several problems with the existing dependency management via
npm.json and npm.py.

**No control over importmap key**

Example: Newer Three.js versions come with three.module.js which points
to three.core.js. We can declare both files as a dependency, but they
use the same key "three" in the importmap. A similar problem came up in
#4163.

**"Manual" code modifications needed**

If a file like three.module.js references `./three.core.js`, we have to
replace the relative link because the file three.core.js isn't found at
this relative URL. Similarly there is an import of
`../utils/BufferGeometryUtils.js` in Three.js' GLTFLoader which we need
to replace with `BufferGeometryUtils`. These corrections happen in
npm.py.

**Complex dependencies hard or impossible to integrate**

There is an extra script at scripts/codemirror/bundle.bash to integrate
CodeMirror. And we wanted to integrate ajv-formats for `ui.json_editor`
(https://cdn.jsdelivr.net/npm/[email protected]/+esm), but it includes
references to e.g. "/npm/[email protected]/+esm" which don't work in a local
NiceGUI app (see #4749).

### Implementation

For this PR I decided to move modules with dependencies into separate
directories (e.g. nicegui/elements/scene/) instead of holding all
dependencies in one giant lib/ directory. This includes codemirror,
scene, aggrid, joystick, json_editor, plotly, echarts, leaflet and
mermaid.

Each of these subdirectories contains an independent package.json,
bundler configuration (rollup or vite), src/ directory with an index.js,
a Git-ignored node-modules/ directory, and a generated dist/ directory
with the resulting (sometimes chunked) bundle. This way we can configure
the build individually for each UI element and don't have to find a
one-fits-all solution. Furthermore, we can rely on standard NPM tools
without the need for our own custom JSON format and Python scripts for
downloading and extracting packages. Now the build process is 1. `npm
install` and 2. `npm run build`.

For registering the module in NiceGUI, I introduced a new `esm`
parameter when subclassing an `Element`, e.g.:
```py
class JsonEditor(Element, component='json_editor.js', esm={'nicegui-json-editor': 'dist'}):
    ...
```
This defines an entry to the importmap named "nicegui-json-editor"
pointing to the relative directory dist/. This replaces the definition
of individual `dependencies=[...]`, which we will probably deprecate.

### Progress

The PR is still work in progress. The change has quite some implications
that still need to be implemented.

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
    - [x] remove deprecated parameters from `Element.__init_subclass__`
    - [x] create custom package.jsons per UI element
- [x] create package.json for NiceGUI's core dependencies (Vue, Quasar,
Tailwind)
    - [x] remove npm.json/npm.py
- [x] update examples for custom components (number_checker,
signature_pad)
    - [x] update gitattributes
    - [x] generate DEPENDENCIES.md?
    - [x] what about minification and maps?
- [x] Pytests are not necessary.
- [x] Documentation has been updated.
This PR removes code and APIs which have been marked as deprecated.
Exceptions:

- Since we probably won't update to Tailwind 4, the documentation links
to version 3 remain unchanged.
- Parameters for subclassing `ui.element` are already updated in PR
#5021.
- `previous_value` will only be enforced in NiceGUI 4
### Motivation

As noticed by @M6stafa in #5045, Quasar elements and the connection
popup haven't been rendered correctly for languages with right-to-left
text direction.

### Implementation

This PR loads the quasar.rtl*.css files and adjusts the CSS rules for
the connection popup for `:dir(ltr)` and `:dir(rtl)` respectively.

It can be tested with some input like
```py
ui.input(label='متن ورودی', placeholder='متن خود را وارد کنیم')
```

and choosing a language like
```py
ui.run(language='fa-IR')
```

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests are not necessary.
- [x] Documentation is not necessary.
### Motivation

NiceGUI 3.0 should upgrade its Tailwind dependency to version 4 which
has been released quite a while ago.

### Implementation

This PR had to address several challenges:

- update package.json and extract_core_libraries to use Tailwind from
node_modules/ instead of http://cdn.tailwindcss.com/
- remove warning about not using this version in production
- introduce CSS layers to properly integrate Tailwind with Quasar and
NiceGUI's CSS rules
- update our hack to preserve borders of card children
- fix a conflict between Tailwind's "inline" class a Quasar components
like QSelect, QCheckbox and QRadio
- update documentation and tests to use new border classes
- update links in documentation
- ~~update~~ remove the `.tailwind` API

An open question is what to do with the `.tailwind` API: It is getting
more and more useless after Tailwind removed classes like "p-0" and
mainly kept generic classes like "p-<number>", "p-(<custom-property>)",
and "p-[<value>]". We could try to improve the API to support numbers
beside literal strings, but it would require significant work. Apart
from that the current PR causes several breaking changes because, e.g.,
[borders](https://tailwindcss.com/docs/border-style) now can start with
"border-" _or_ "divide-". Therefore the literal string options change
from "solid" etc. to "border-solid", "divide-solid" etc. It feels like
Tailwind is getting to complex and flexible to keep trying to mirror its
API. A potential way forward could be to release the new API as
deprecated and discourage using it until we shut it down in NiceGUI 4.0.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests have been added (or are not necessary).
- [x] Documentation has been added (or is not necessary).
- [x] Remove the `.tailwind` API.
#4978)

### Motivation

When reviewing PR #4969, we noticed that it would be handy if the props
dictionary would automatically update the frontend when changed:
```py
b = ui.button('test', on_click=lambda: b.props.update(icon='face'))
```

### Implementation

This PR replaces the `dict` base class with an `ObservableDict` and
calls a new `_update` method. In order to avoid keeping strong reference
to `element`, this method needs to use the weak reference
`self._element`.

I'm marking the PR as draft because we should carefully evaluate memory
and performance, add pytests and documentation. Although the change
shouldn't break anything, it might be a good idea to release this
feature in 3.0.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Introduce `@helpers.prevent_recursion`
- [x] Existing pytests are passing.
- [x] Pytests have been added.
- [x] Documentation has been added.
- [x] This PR breaks
https://nicegui.io/documentation/table#show_and_hide_columns [BREAKING
CHANGE].
- [x] The condition `if self._send_update_on_value_change:` might be
ineffective now.
- [x] `test_apply_format_on_blur` is red
### Motivation

When refactoring or renaming variables their string names in bindings
are often missed since the rename IDE action doesn't pick it up. We
discussed creating a method that gets the variable name as a string from
a reference which proved possible but costly. As an alternative to raise
awareness to missed variable names after renaming we propose to add an
option that checks if the binding target exists and warns if it's not.

### Implementation

The implementation is rather straight forward and involves a lot of
trivial parameter adding. The check takes place in the functions `bind`,
`bind_to` and `bind_from` in `bindings.py` which are used in all the
other occurrences.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
  - [x] update all docstrings
  - [x] `direction` could be a `Literal`
  - [x] check both, source and target
- [x] change it to `check: bool | None = None` and check non-dicts by
default
- [x] Pytests have been added.
- [x] Documentation has been added.

---------

Co-authored-by: Falko Schindler <[email protected]>
…5104)

### Motivation

I stumbled upon the built-in function `str.translate` which can be used
to replace multiple characters more efficiently than calling
`str.replace` repeatedly. This reminded me of our
`client.build_response` method which calls `str.replace` five times to
escape special characters for HTML.

### Implementation

`client.build_response` is further optimized by globally defining a
translation dictionary which is re-used by every client.
~~While being at it, two other occurrences of repeated `str.replace`
calls have been replaced.~~

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Pytests are not necessary.
- [x] Documentation is not necessary.
…he code (#5107)

### Motivation

While working on PR #5005, we noticed that some projects like our ROS2
example might depend on the shared nature of the long-living auto-index
client. If there is some kind of "event" in the data model (or hardware
controller), they can simply update the UI if there is only one and
exactly one client. When switching to page functions, they updating UI
becomes more complicated in this scenario.

Therefore we decided to move our event system from
[RoSys](https://github.com/zauberzeug/rosys/) to NiceGUI. It provides a
simple API to define events, subscribe arbitrary callbacks to them, and
to emit/call them if the event happens.

### Implementation

The core implementation is taken from RoSys. But on a second look some
details have changed, partly by relaxing some assumptions or because we
can make better use of NiceGUI's infrastructure.

- I chose the method names "subscribe" and "subscribe_ui" instead of
"register" and "register_ui".
- Callbacks can still be sync or async.
- Callbacks can - but don't have to - receive arguments.
- Callbacks are automatically called within the same slot where they
subscribed.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] The implementation is complete.
- [x] Merge `subscribe` and `subscribe_ui` into a single `subscribe(...,
unsubscribe_on_disconnect: bool | None = None)`.
- [x] Pytests have been added.
- [x] Documentation has been added.
…tional root parameter in ui.run (#5005)

### Motivation

As discussed in #4964, we could streamline the the setup of NiceGUI as
an SPA by providing a positional `root` parameter in `ui.run`. The basic
pattern becomes so simple that we do not need auto-index pages anymore 🎉

### Implementation

This PR implements an idea form @falkoschindler and me while discussing
#4964: by evaluating the `root` builder function in the FastAPI default
404 handler we do not need to register catch-all FastAPI routes at all.
Thereby any other defined FastAPI routes take precedence. Only if the
FastAPI routing fails we route the path to the `root` builder function
passed via `ui.run`. A basic (already working) example looks like this:

```py
from nicegui import ui

def root():
    ui.link('Go to main page', '/')
    ui.link('Go to other page', '/other')
    ui.sub_pages({
        '/': main,
        '/other': other,
    })

def main():
    ui.label('Main page content')

def other():
    ui.label('Another page content')

@ui.page('/test')
def test():
    ui.label('Test page content')

ui.run(root)
```

Of course that only works when auto-index page does not register a `/`
page on it's own.

### Progress

- [x] I chose a meaningful title that completes the sentence: "If
applied, this PR will..."
- [x] Finish `root` implementation:
  - [x] use new parameter for `ui.sub_pages` demos
  - [x] use page-creation code 
  - [x] provide support for async root pages
- [x] inject required parameters into root page similar to what
ui.sub_pages does
- [x] either make fallback to `root` builder behave exactly like
`ui.page` or ensure via tests that both are working as expected
  - [x] `root` parameter for `ui.run_with`
- [x] remove auto-index client
  - [x] `client.request` is never `None`
  - [x] remove `client.shared`
  - [x] remove `individual_target`
  - [x] documentation not starting
  - [x] support NiceGUI scripts
  - [x] remove `# @ui.page('/')` hack in demos (not needed anymore?)
  - [x] remove "Needs page builder function" in storage documentation
- [x] fix app.storage.user for scripts (and client, tab, browser,
general, and update documentation)
  - [x] fix pytests still refering to `auto_index_client`
  - [x] fix ROS2 example (using a new event system?)
  - [x] raise when combining script mode with `ui.page`
- [x] Pytests have been added.
- [x] Documentation has been updated.

---------

Co-authored-by: Falko Schindler <[email protected]>
@falkoschindler falkoschindler added this to the 3.0 milestone Sep 4, 2025
@evnchn

This comment was marked as resolved.

@evnchn
Copy link
Collaborator

evnchn commented Sep 4, 2025

Also can we potentially make the searching a bit easier by re-phrasing "upstream breaking change" to "upstream changes which broke our code" in old release notes, so that searching for "break" or "breaking" yields only the breaking changes?

Or the other way round with "our breaking changes" so that we can search by "our break" / "our breaking"

@evnchn

This comment was marked as resolved.

@falkoschindler
Copy link
Contributor Author

@evnchn We updated the release notes to include deprecations.

Updating old release notes is an interesting idea. But it will probably break (pun not intended) in the future as soon as someone forgets that "break" is a forbidden word for non-breaking changes.
Alternatively you can search for "⚠️". I just don't understand why v2.7.0 is popping up. Maybe because of my falcepalm 🤦🏻‍♂️.

@evnchn
Copy link
Collaborator

evnchn commented Sep 5, 2025

I read through the release notes.

Although may not affect everyone, aren't the JavaScript version bumps also potentially breaking?

I think I'll do a search.

@evnchn
Copy link
Collaborator

evnchn commented Sep 5, 2025

THIS MAY NOT BE ACCURATE!!!

JavaScript dependencies with major version bump:

JavaScript dependencies with minor version bump:

New JavaScript dependencies:

  • @codemirror/language-data: 6.5.1
  • @codemirror/theme-one-dark: 6.1.3
  • @uiw/codemirror-themes-all: 4.24.2
  • @babel/runtime: 7.28.2

Unchanged JavaScript dependencies:

  • echarts-gl: 2.0.9 (unchanged)
  • nipplejs: 0.10.2 (unchanged)
  • leaflet: 1.9.4 (unchanged)
  • leaflet-draw: 1.0.4 (unchanged)
  • @tweenjs/tween.js: 25.0.0 (unchanged)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Client-Side Memory Leak AG-Grid's 'rowSelection': {'mode':"multiRow",'headerCheckbox':False} won't work
5 participants