Skip to main content

Nuiitivet

Nuiitivet showcase

An intuitive UI framework for Python.

PyPI version Python versions License

1. Why Nuiitivet?

I have just one thing to say: I want to write UI intuitively.

1.1 Declarative UI

Nuiitivet offers a declarative UI that blends the best parts of frameworks like Flutter, SwiftUI, and WPF.

At its core, you build UIs by composing widgets, just like in Flutter.

login_form = Column(
    [
        # Username and Password fields
        TextField(
            value="",
            label="Username",
            width=300,
        ),
        TextField(
            value="",
            label="Password",
            width=300,
        ),
        # Login Button
        Button(
            "Login",
            on_click=lambda: print("Login clicked"),
            width=300,
        )
    ],
    gap=20,
    padding=20,
)

Login form

What sets Nuiitivet apart from Flutter is that size, alignment, and spacing are specified as widget parameters. Treating them as parameters of a widget — rather than as widgets in their own right — feels more natural, and it lets you avoid the deep nesting hell that Flutter tends to fall into.

# Writing in Flutter style often leads to deep nesting
Padding(
    padding=EdgeInsets.all(12),
    child=SizedBox(
        width=200,
        child=Text("Hello"),
    ),
)
# With Nuiitivet, you can specify them directly
Text(
    "Hello",
    padding=12,
    width=200,
)

Nuiitivet also adopts modifiers from SwiftUI and Jetpack Compose. Instead of wrapping a widget, you attach decoration and behavior so they feel like they grow out of the widget — and they chain together naturally with |.

Button("OK").modifier(
    tooltip("Submit") | clickable(...) | background("#2196F3")
)

For why modifiers exist and what kinds are available, see docs/guide/modifiers/index.md.

1.2 Data Binding

Dynamic UIs need state management. With data binding, you declare what the UI shows in terms of your state — once — and that link stays live. Change the state, and every bound part of the UI follows on its own. You never write the code that pushes a value into a widget, and the UI can't drift out of sync with your state, because your state is the UI's single source of truth.

That mechanism is Observable. It binds directly to the UI, and it also carries operators like throttle() and debounce() like Rx — the best of both worlds. (It's inspired by WPF's ReactiveProperty.)

Let me walk you through three things I like about it.

1. Complete separation of state and UI

When you set a value on an Observable, the bound UI updates automatically. Inside build(), all you ever write is the UI declaration.

class CounterApp(ComposableWidget):
    def __init__(self):
        super().__init__()
        self.count = Observable(0)

    def increment(self):
        self.count.value += 1

    def build(self):
        return Column(
            [
                # Count display
                Text(self.count),
                # Increment button
                Button(
                    "Increment",
                    on_click=self.increment,
                )
            ]
        )

Counter

With the ViewModel pattern, you can take this even further — separating cleanly at the class level rather than the method level.

2. Declarative data flow

State derived from multiple values can be declared as a formula. Below, total is defined as the sum of count_a and count_b; whenever either one changes, it's recalculated automatically. On the UI side, you just drop in total as is.

self.count_a = Observable(0)
self.count_b = Observable(0)

# total is declared as a + b; it recalculates automatically when a or b changes
self.total = self.count_a.combine(self.count_b).compute(lambda a, b: a + b)

Multi counter

3. Async in the same style

This is where the ReactiveProperty heritage really shines. You can slot in an Rx-style operator like debounce() and then bind the result straight to the UI. Even something like a search box — "thin out the keystrokes, then process" — is a single line.

self.query = Observable("")

# debounce like Rx, then bind the result straight to the UI
self.results = self.query.debounce(0.3).map(search_api)

The full guide to Observable is in docs/guide/state-management/index.md.

1.3 Event Handlers

Event handlers like on_click() are written imperatively. A handler does procedural things — popping up a dialog, branching on its result — so writing it imperatively feels natural.

class CounterApp(ComposableWidget):
    count = Observable(0)

    # Write procedures in event handler
    def handle_increment(self):
        # 1. Output log
        print(f"Current count: {self.count.value}")
        # 2. Increment count
        self.count.value += 1
        # 3. Milestone check
        if self.count.value % 10 == 0:
            print("Milestone reached!")
        
    def build(self):
        return Column(
            [
                Text(f"count: {self.count.value}"),
                Button(
                    "Increment",
                    on_click=self.handle_increment,  # Execute on click
                )
            ]
        )

Logic → UI declaratively, UI → logic imperatively. What matters in both directions is that it stays intuitive to write — and that's the one thing Nuiitivet stands for.

2. First Steps

2.1. Requirements

  • Python 3.10 or higher
  • macOS / Windows / Linux

Main internal libraries used (drawing/rendering):

  • pyglet
  • PyOpenGL
  • skia-python
  • materialyoucolor

See LICENSES/ for third-party licenses.

2.2. Installation

You can install it easily with pip.

pip install nuiitivet

2.3. Your First App

To create an application with Nuiitivet, follow these steps:

  • Import your UI design system with import nuiitivet.material as nv
  • Inherit from ComposableWidget to create a UI component
  • Pass the UI component to App and start the application
import nuiitivet.material as nv

class CounterApp(nv.ComposableWidget):
    def __init__(self):
        super().__init__()
        self.count = nv.Observable(0)

    def handle_increment(self):
        # 1. Output log
        print(f"Current count: {self.count.value}")
        # 2. Increment count
        self.count.value += 1
        # 3. Milestone check
        if self.count.value % 10 == 0:
            print("Milestone reached!")
        
    def build(self):
        return nv.Column(
            [
                nv.Text(self.count),
                nv.Button(
                    "Increment",
                    on_click=self.handle_increment,
                )
            ],
            gap=20,
            padding=20,
        )

def main():
    # Start with App (pass the class as a factory so hot reload can rebuild it)
    app = nv.App(content=CounterApp)
    app.run()

if __name__ == "__main__":
    main()

2.4 AI pair-programming

Once your app runs, you develop it in a live loop built for pairing with an AI assistant. Launch with the dev runner instead of running the module directly:

python -m nuiitivet.dev path/to/app.py

Now you and the assistant work on the same running window:

  • You watch the assistant work in real time. Every edit it makes and every screen it drives shows up live — the window rebuilds in place on each save and your Observable state survives, and even a screen you are stepping through under the VSCode F5 debugger keeps updating. No restart, no lost state.
  • Your turn is hands-on too. You direct the assistant, but you can just as well edit code and manually test the screen yourself in the same session.
  • The assistant sees what you did. Beyond your instructions, it can read which files you changed and which UI actions you took, so you stay on the same page and the conversation gets sharper.

Three pieces make this work:

  • Hot reload: real-time screen updates.
  • MCP Dev Bridge: lets the assistant read and drive the running app, and read your edit/interaction logs.
  • nuiitivet-app skill: an AI skill that keeps the assistant's code idiomatic.

See AI pair-programming for the full workflow.

3. Documentation

For a deep dive into Nuiitivet's design, visit the docs site. Browse runnable examples in samples/ — every snippet in this README lives there as a runnable module under samples/readme/.

Core Concepts

Guide Summary
Layout Build UIs with widgets and parameters.
State Management Reactive Observable state that auto-updates the UI.
Modifiers Attach decoration and behavior to widgets.
UI Design System Theming and design tokens.

Building Screens

Guide Summary
Overlay Dialogs, loading, and overlays.
Navigation Screens, routes, and transitions.
Window & Chrome Window sizing, position, and custom chrome.

Material Design

Guide Summary
Material App App entry point and structure.
Material Theme Color schemes generated from a seed.
Material Widgets Catalog of built-in widgets.

Going Further

Guide Summary
Async & Threading Safe UI updates from background work.
AI pair-programming Live edit-save-see, the MCP dev bridge, and the nuiitivet-app skill.
Packaging Ship your app to users.

4. Known Limitations

  • No OS accessibility integration. Everything is drawn with Skia, so the app does not participate in the OS accessibility tree — screen readers and VoiceOver cannot inspect the UI. This is a real constraint for domains that require assistive-technology support.
  • A GPU is recommended, not required. Live rendering goes through pyglet + PyOpenGL + skia, and by default it uses an OpenGL/GPU context. On GPU-less, software-OpenGL (llvmpipe), or remote setups it falls back to CPU/raster rendering, which you can also select explicitly — see Renderer Selection.
  • A display is required. App.run() opens an OS window, so truly headless environments (no display at all) are not supported in any renderer mode.

5. License

Nuiitivet is licensed under the Apache License 2.0. See the LICENSE file for more info.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

nuiitivet-0.14.0.tar.gz (16.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

nuiitivet-0.14.0-py3-none-any.whl (16.7 MB view details)

Uploaded Python 3

File details

Details for the file nuiitivet-0.14.0.tar.gz.

File metadata

  • Download URL: nuiitivet-0.14.0.tar.gz
  • Upload date:
  • Size: 16.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for nuiitivet-0.14.0.tar.gz
Algorithm Hash digest
SHA256 519b9844e646303a320441249d99741a9c62b77a9986542fc0e81f399c5f9029
MD5 5bac70fc5426bc2a608b3e4bd6a0b9ec
BLAKE2b-256 dcedc8afada45c847b0603e70a8ddc949b15db3ef09ee22b90a48a3e2ce43ae5

See more details on using hashes here.

Provenance

The following attestation bundles were made for nuiitivet-0.14.0.tar.gz:

Publisher: release.yml on yuksblog/nuiitivet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file nuiitivet-0.14.0-py3-none-any.whl.

File metadata

  • Download URL: nuiitivet-0.14.0-py3-none-any.whl
  • Upload date:
  • Size: 16.7 MB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for nuiitivet-0.14.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a5339a50bbdd775983eadce4ce112fb0566c95c832cc4c8d25446d4c66b65270
MD5 9b06752e43015353fae0b6ebdf9fad4c
BLAKE2b-256 a4ba040c7ad968a2f1a306bed220605f5064887b772ae3a72029a5ac5403cc80

See more details on using hashes here.

Provenance

The following attestation bundles were made for nuiitivet-0.14.0-py3-none-any.whl:

Publisher: release.yml on yuksblog/nuiitivet

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.22.0

2 files

0.21.1

2 files

0.21.0

2 files

0.20.1

2 files

0.20.0

2 files

0.19.0

2 files

0.18.0

2 files

0.17.1

2 files

0.17.0

2 files

0.16.0

2 files

0.15.1

2 files

0.15.0

2 files

This release

0.14.0 This release

2 files

0.13.0

2 files

0.12.0

2 files

0.11.2

2 files

0.11.1

2 files

0.11.0

2 files

0.10.0

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.1

2 files

0.7.0

2 files

0.6.0

2 files

0.5.3

2 files

0.5.2

2 files

0.5.1

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.2

2 files

0.3.0

2 files

0.2.2

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page