5 Years of NiceGUI: What We Learned About Designing Pythonic UIs
NiceGUI is a Python-based web UI framework designed to simplify the creation of graphical interfaces for robotics, automation, and general-purpose applications. It addresses the complexity of traditional web development by allowing developers to define UIs directly in Python, avoiding the need for separate HTML, CSS, and JavaScript files. Unlike frameworks that re-run the entire script on every interaction, NiceGUI maintains a stateful connection via WebSockets, enabling more complex layouts and responsive behavior.
The framework employs several specific design patterns to ensure a Pythonic developer experience. It uses context managers (the with statement) to define hierarchical UI structures, mirroring the nesting of HTML elements. To reduce boilerplate, it implements a Fluent API through method chaining, allowing developers to configure element styles and event handlers in a single statement. Event handlers are supported via lambda expressions for lightweight actions and can be asynchronous to prevent the UI from freezing during background tasks. For complex updates, the UIRefreshable decorator allows specific sections of the UI to be updated without clearing the entire container.
Data integration is handled through a binding module that links UI elements to Python data models, such as data classes or dictionaries, supporting both one-way and two-way binding. Technically, NiceGUI is built as a FastAPI application and leverages the Quasar framework and Tailwind CSS for styling. It provides escape hatches for advanced users, allowing the integration of custom HTML, CSS, and JavaScript, or the attachment of the UI to an existing FastAPI backend.
This description was generated by Open-Source AI using the transcript of the session and the original submission contents.
This session took place in track Programming & Software Engineering & Testing and was classified suitable for intermediate domain / intermediate python by the speaker.
Submission
The proposal as submitted by the speaker before the conference.
Five years ago, the NiceGUI project set out to answer a simple question: Can we build modern, interactive web UIs entirely in Python without giving up power or flexibility? Since then, the framework has evolved into a production-ready, community-driven tool that builds on top of proven technologies such as HTML, CSS, JavaScript, Vue.js, Quasar, Tailwind, and FastAPI—while exposing a Pythonic interface that feels natural to Python developers. This talk traces that journey and distills the design principles that worked, those that didn’t, and the patterns that ultimately enabled NiceGUI to provide a smooth developer experience. We begin with a short demonstration of NiceGUI’s “3-line Hello World,” highlighting how familiar Python code can generate dynamic web interfaces. From there, we examine the technical foundations that allow the framework to stand on the shoulders of major frontend and backend ecosystems. The core of the talk focuses on Python language features and how they shape API design:
- Context managers to express hierarchy and UI composition intuitively.
- Method chaining inspired by the builder pattern for concise, readable configuration.
- Decorators (such as @page and @refreshable) to define routing and reactive behaviour without ceremony.
- Async/await for event handlers, background tasks, and page functions.
- Type hints to support static analysis, IDE completion, and clearer API intent.
- Dataclasses as bindable, structured state containers.
- Default arguments and sentinel patterns to allow powerful yet discoverable APIs.
Attendees will gain practical insights useful beyond NiceGUI itself: how to design Python APIs for GUI frameworks, dashboards, developer tools, or any domain where clarity, maintainability, and expressiveness matter. The talk is aimed at Python developers interested in web interfaces, framework design, or improving the ergonomics of their own libraries.
Transcript (auto)
Auto-generated from the recording utilizing Open-Source AI. Speaker labels (Speaker 1, Speaker 2) reflect diarization, not identity. Timestamps refer to the recording.
Speaker 1 [00:00]
Five years of NICE GUI, what we learned about designing Pythonic user interfaces. So we are going to hear from Falco Schindler, and he is a software engineer and a creator of the open source web UI framework, NICE GUI. He specializes in building the company's core software stack for robotics and automation projects. Please give him a hand clap as he takes the stage.
Speaker 2 [00:30]
Thank you very much for the introduction. Welcome to my talk. It's actually pronounced nice guy. We chose this pun. I'll explain a bit later about it. Yeah, it's been five years already, when we started building this web UI for Python. But first, some background about myself. I'm Falco. I'm working at a small company called Zauberzeug, that's a German word for magic tools, in Münster, in Germany. And we do small software projects, but also robotics. One of our robots is the Field Friend, an agricultural robot for weeding. and we had the problem of how can we interact with our robots or in the beginning only some parts of it some hardware some motor controllers we wanted to fine-tune them so we needed a way to some graphical user interface and ideally over the network that's why we thought about couldn't we simply build some kind of toolbox for Python where we can specify what we need in Python directly and web server would serve the UI to the browser and then we could interact from browser to client to server by our web sockets and this was the idea for a new web framework and in the beginning we were looking around for existing solutions and here I want to compare some options we had and to show the difference in how the implementation would look like I want to create this simple UI, a button next to a label inside a card, like a container, and whenever I click the button, the label should update. Very simple. And one library we found, after we developed the idea, we found a library called JustPy, And there you could write all UI code in Python, just in Python, but we didn't like it too much because it required you to set up quite a lot of things. You had to define a page, then you can define the card inside. You can define a row, which is a div. Now as a robotics engineer, you now are wondering, what is a div, what do I need a div for? So it's very closely aligned with web development, and nowadays there's also fast HTML, another framework which looks a little bit like this, though there are options in Python where you can write HTML, okay, but it's a bit low level. And on the other side, there's also the pretty famous Streamlit library, which allows you to write simple UIs in very little code. That's really nice. But if you look closely, they are kind of, yeah, it's a bit unconventional how they wire buttons to actions. They write if button, then something should be written. And this only works because on every user interaction, they re-evaluate the code, re-run your script. So when the button is clicked, then the condition is true, and then the execution steps into this block. And again, this works nicely for simple UIs or maybe data dashboards, the domain where Streamlit was built for. but as soon as you want a bit more specific layout like we want a button next to the label layouting gets a bit more complicated and if you have stateful UI then you have to use some session state and have to manually take care of when to rerun the script and I'm not 100% sure that this is the perfect solution for this problem, but I repeatedly hear that Streamlit users struggle with session state once they reach some certain complexity. And then we have been in the nice position to be able to dream about what would a nice Python UI framework look like, how would we like to write UI. And this is what we came up with. We borrowed the idea of nesting elements with statements. And we tried to remove boilerplate code where possible, though the manual wiring of adding instantiated elements to containers is gone because you simply instantiate them inside the context of their container. We encourage the use of lambda expressions, so you do not need to define functions explicitly, and so on and so on. And this is exactly what is running here. This is exactly this code. And whenever you click the button, the label changes. Now I want to look into the individual design decisions more closely, one of which was, as already mentioned, using hierarchical code structures to define hierarchical UIs, which which is basically borrowed from HTML, where you also nest elements inside others. This is done with the with statement in Python, which you could see as a within statement. You can't rename the keyword, but it's basically a within statement. This is not only useful in our library, it translates to other scenarios as well. You can use the statement nicely to mirror the shape of your data. Usually, UI elements come with many options for configuration. Like in Nice Guy, you have many options to manipulate the look and feel of the styling via event handlers, like when I hover this button. And pretty quickly, the nice code structure gets a bit messy again. And this is where we chose to embrace method chaining, simply by returning self again and again. You can write UI like this. Now you see that the card contains a button and a label at a glimpse. And the configuration now reads like a bullet point list. Every line has one purpose. The button is configured to only show with an outline. With the props method, you can add custom CSS if you choose to. And every event handler is also one line. And this is also called Fluent API. So this lets you create and configure elements in one statement, which allows you to leave out the local variable. So you don't need to name the button. You can simply put it there, and it's done. And by encapsulating functionality in several methods like props and style and not putting all configuration options into the initializer, we achieve a separation. And the beginner can only use the button initializer, and the advanced user can tap into more specific options. So you don't have to learn the whole library at once. You've already seen event handlers, and like in the JASPA example, you usually would define named functions and pass them into the handler, into the initializer in this case. But again, this already blows up the code quite a lot. So using lambda statements, we can reduce this example to only three lines, three elements, three lines. And whenever I change the number, the notification is shown. And whenever I click the button, the label is changed again. And this is possible because the library provides ways to, for example, set the text within a lambda expression because usually if we simply want to assign a new label a text we can't do that inside of lambda expressions because they don't allow assignments but they allow method calls and this is why nice guy chose to provide set methods everywhere and also nice guy automatically passes in event handlers if you want to use them or you can simply omit them then it's fine as well so you don't have to add noise to your code which you don't need and two more things about event handlers event handlers can be asynchronous of course and then nice guy will await them in the background automatically in the background that's important because the ui needs to be needs to remain responsive while the tasks are running in the background. And which might seem like an edge case, but consider you have buttons in separate contexts on separate cards, like here. Each button knows where to add the new element. I'm told him to do. Here it's maybe a bit playful, but that's It's a very important concept if you think about that this is a web framework. Users can access from multiple browsers at the same time. You're running in the browser in the end. So every user should act in their local scope, in their local context. So an event from one user shouldn't affect the other user, and that's why it's important that NiceGuy always takes care of the current context. In the end, our insight was that callbacks should be as lightweight as the action they describe. You don't want to add much ceremony just because you want to show a notification or something. And sometimes, event handlers get a bit more complex. Then maybe you have a slider. And whenever the value changes, you want to update some UI. And the UI is here wrapped in a function. And this is a pattern we noticed quite often, that you define your UI, but then you want to update it. And you do it by clearing the whole container. And within the container, you recreate the UI. And to avoid repeating this pattern again and again, this sounds like a good opportunity for a decorator. Because if you repeat the same scaffolding again and again, you might think about adding a decorator. And what this describes is basically some refreshable UI. So we introduced UIRefreshable, which you can add to a function. And now it reads more like a description of how would part of your UI look like. And you're making it refreshable. Now, after you called it once, you then can simply call display.refresh, and this will refresh every instance of this UI on your page, in your scope, or even across users, if you choose to. Yeah, and this way, you can reduce the boilerplate of your user code. Another pattern which appears quite often is that you want to link your elements to each other. Maybe a number is displayed as a label text again and also adjustable with a slider. Usually you would wire them using quite some event handlers. On change you want to set the label text. The label needs to be initialized correctly and the slider again. quite some wiring to do and this is often a good opportunity for binding and nice guy also comes with a built-in binding module so you simply can say that the label should bind its text from the number more specifically from the value attribute from the number element and you can also optionally specify a transformer function if If you want to maybe, like here, convert the number into text with unit like this. And the slider is simply bound in a two-way binding from the number element. And this doesn't only work between UI elements, but also with data models, like a data class, for example. Here I am defining a data class temperature with only one attribute value and again you can simply bind the UI elements to the data model and the result is basically the same but now you have access to the data model in your backend code if you want to say so. And this is particularly nice because other UI elements tend to force you into using their classes. Sometimes you have to kind of rewrite all your business logic just to make it bindable and usable in your UI. Here you can simply use a Python object model. It can be a data class, it can be a regular class, or even dictionaries, and it just works. Now I want to leave the runtime a little bit, and we come to a pretty tricky part. I hope I can I can explain it to you nicely. When designing a library like this, we always think about the developer experience when using it inside the IDE. If you think about an example like this, we just want to place a button. The button usually comes with a default color. This is our, you've seen it here, our nice guy blue. This is also called the primary color. You can modify it globally, but usually the button is blue. You can say the button should be red. OK, then the button is red. No discussion. But there's also a concept, which we added on demand, that you can specify attributes for your elements in your whole app. For example, you might want to place every button with sharp corners and not rounded, or every button should be red in your app. Then you can use default props. This all has to do with the underlying Quasar framework, which talks about props. So you can say every button should have red color. OK, then this button, of course, also is red. And now comes the tricky part where you said every button should be red, but this button should be using primary color. And now the question is, for the user, which wins? Is this value used or not? And the more important question is, how can the framework know inside the button's initializer, is this value the default parameter value, and the user didn't change it, so we should default to the default prop, or is this value explicitly passed in by the user and we should use primary color? And there's no really nice solution for it, for this specific distinction, but you can use special values as placeholders for the values missing. There was a talk two days ago, very interesting, about exactly this topic, using kind of sentinels. Sometimes none is used as a sentinel, but it can have conflicts for certain parameters if none has a special meaning in this context. Maybe none means a transparent color, and then none is a bad choice for the sentinel. But you can specify a custom sentinel, like a new object and use it there. But two problems remain. One is typing. Our sentinel is not a string, and so the type checker isn't happy about this. So you would extend the type, which is messy again. And more importantly, what bothered me for quite a long time, the user doesn't immediately see from the signature what is the default color. If I add a button, then I do not see that it will use the primary color. After some iterations, we came up with this solution, that we introduced a custom default prop Sentinel, which overrides the Dunder or operator, so that we can combine it with the usual default color, like in this case, and then combined with a decorator, resolveDefaults, the value can be resolved at runtime. Whenever we instantiate a button, the decorator can check, has there been a default prop? If not, then the primary color is used. Or did the user pass in an explicit value? Then it won't be the sentinel here. Then it will be a string which is passed in, and the decorator will resolve it. Everything here is just library code, so as a user you don't have to bother with it. But it shows how deep... I personally like to go into the language features and try to find a way to make it pleasurable for the user. And in the end, this is the result, what the user sees. So by hovering the button, you see immediately it's impacted by default props, but it's usually a primary color. So to summarize, the best documentation is the one users never have to open. Sure, we also come with a pretty extensive documentation, but it's always best if the user sees it immediately inside the IDE. Let's step out a bit. NiceGuy is built on top of many powerful frameworks and concepts. And one important thing, which turned out pretty powerful for our users, is that you always can step out of NiceGuy into the underlying layers. so our users can build things we hadn't even in mind just because they can use the power of HTML for example using a custom special UI HTML element for custom HTML code adding CSS definitions to elements using Tailwind. Tailwind is another library which is tightly coupled with NiceGuy. I already mentioned the the Quasar framework, which can be used. We can run custom JavaScript code if you wish to. And as I mentioned, NiceGuy is a fast API app. So you can specify your own endpoints or do whatever you want to do with fast API. You can even attach a NiceGuy UI to an existing fast API app if you already have one. And of course, we are in Python land, So we can use whatever Python has to offer, like defining custom elements, element classes, and so on. So we try to always provide a path to the layer below, which makes NiceCAD pretty powerful. I basically skipped this slide, which basically repeats these insights we already had, just concluding that most of it isn't just about UI, but it's about designing Python APIs. So many concepts you can use for other applications as well. As I mentioned, nice guy, the pun is intended. A nice guy usually is someone who tries to be nice always and do things right. The nice guy library also tries to be nice to the users as well as to the developers. We always strive for simple solutions in code reviews. Even if a complex solution already exists, we try to find the nice one. Of course, we are fostering a welcoming open source culture on GitHub. Everyone is welcome. We are happy about every feedback or question or contribution. And as a proof to ourselves that Nice Guy is nice to use and scalable and powerful. We built the whole NiceGuy website with NiceGuy, of course. And it contains hundreds of interactive examples like in these slides, which, by the way, also are built with NiceGuy, because why not? I'd like to conclude my talk with this last code example and a quote from Alan Kay, which summarizes it quite well. Simple things should be simple, and complex things should be possible. I thank you for your attention, and I'm here for questions.
Speaker 1 [25:09]
All right, we're going to be taking in questions. I cannot see any questions at the moment, but if you're in here and you have a question, You can just raise your hand, and we shall get the mic to you. Thank you. All right. So the first question is, are there integrations for authentication available, especially in regards to SSO integrations with identity providers, e.g., Google, Azure, and TrueID?
Speaker 2 [26:00]
Yes, there are basically, we have, as I mentioned, lots of interactive examples on our documentation, but there is also a collection of a bit larger examples. In our Git repo, there's an examples folder, and there we also have authentication examples also for Google and maybe one or two more. not all of them but there you can see how you would wire an iSky app with these services. So yes it's possible and we are always open for ideas how to integrate it more tightly but there are recipes basically how to do it.
Speaker 1 [26:44]
Okay, so the next question is, when context managers are the layout API, how to create layouts grammatically, put programmatic creation into functions?
Speaker 2 [26:59]
Okay, I guess the question is about, because you always have to use the with statement, and I only showed examples where you create the container element with the with statement, and then I nested elements inside, but you can always give the container element a name, with UICard as card, and then you can use the card inside functions to add more elements to it. It's basically possible.
Speaker 1 [27:38]
All right, so the next question is, are there integrations for authentication available? I think you've answered this already.
Speaker 2 [27:46]
Yes, I did.
Speaker 1 [27:46]
Yes, I did. Okay. So how does NICE GUI compare with DASH? What are the advantages and disadvantages?
Speaker 2 [27:54]
With Dash? I haven't looked into Dash too much already, but as far as I understand, there are many existing solutions like Cradio and others that are more focused on data dashboards and AI. So I think Nice Guy is a bit more general. So you can extend it easier for your custom application. It comes with maybe a bit more elements like, yeah, I'm not sure if data dashboards come with 3D elements, like this 3D scene here. So I think NASCAR is a bit more flexible.
Speaker 1 [28:55]
Then how to deploy NICE GUI to prod? I think it's production. To production. Yeah.
Speaker 2 [29:03]
Yeah, it's basically, the good thing about it is that we are basically a fast API app, so I can refer you to the FastAPI documentation. So like other fast API apps or web apps, you can host it in Docker containers or on cloud servers. NiceGuy also comes with a Docker container, which you can simply use. and though there's nothing much special about it hosting it
Speaker 1 [29:35]
Okay. Is there a concept of a canvas to do animations in NICE GUI?
Speaker 2 [29:43]
Yeah, it depends on what you are trying to animate. Like here, this 3D scene is running in a canvas using 3JS, so if you look up 3JS, it's a very powerful library for 3D illustrations and animations. We also have a 2D option for interactive SVG graphics. Yeah, but the interesting thing is, often users with a particular problem already have something in mind which they have seen online, like a JavaScript library. And often it's easy to combine the JavaScript library with NiceGuy, either in user code or we integrate it in the library directly. And then you can use the full power of JavaScript with NiceGuy. So I'd say as long as there is a JavaScript solution, it's probably also possible to do it in NiceGuy.
Speaker 1 [30:49]
All right, so the next question is, how did you make the decision to use Tailwind over other CSS libraries?
Speaker 2 [30:59]
Back then, it was hardly inspired by JustPy, which was the existing solution we found, which we also used as a foundation for quite a while. So we implemented NiceGuy on top of JustPy. Later, we removed it from the code base and implemented our own transport layer. But they already used Quasar and Vue and Tailwind. And nowadays I would say it was a good choice because Tailwind is, at least for me, the most common and powerful CSS library. So yeah, I'm pretty happy with this choice.
Speaker 1 [31:40]
So how do you do access control to the pages when adding nice GUI to an existing first API app?
Speaker 2 [31:49]
Sorry, the access control?
Speaker 1 [31:51]
How do you do access control to the pages when adding nice GUI to an existing first API app?
Speaker 2 [32:00]
I'm not quite sure what access you want to control if you so you have an existing fast API you have an existing API fast API app and you're adding nice guy and then you can basically use the same authentication methods I already mentioned the first question
Speaker 1 [32:28]
Okay, so someone is asking whether these slides are on GitHub.
Speaker 2 [32:32]
The framework I'm using to build the slides, I call it NiceDeck. It's also open source. Currently, these slides are not online yet, but I will put them online under the NiceDeck repository. But the slides are in pretalks as PDF, so if you want to look into it, they are already there. You can reach out to me if you want to have the source code or whatever.
Speaker 1 [33:01]
All right, will there be native support for bootstraps someday?
Speaker 2 [33:06]
Bootstrap is another UI framework, I guess. We are thinking hardly about what to do with Quasar because Quasar, the current UI framework, is getting a bit dated, and we are thinking about how could we extend it to other UI frameworks or even replace it. That's an interesting discussion. It's one of the larger undertakings in the future, I guess, but we are always open for ideas.
Speaker 1 [33:42]
Okay. As someone who despises tailwind, how easy is it to integrate your own CSS? Is there a Pythonic way for it?
Speaker 2 [33:51]
A Python equivalent for adding CSS? Not to my knowledge. You can use any Python tooling to add. You've seen the style method so there you can pass in any CSS you wish and you can use Python to make it more comfortable always. We have an option to simply disable Tailwind so we can run Nice Guy without Tailwind if you don't like it. Yes, but I don't know any Python equivalent.
Speaker 1 [34:26]
Okay, this is great for non-programmers. How about those that are more experienced in coding? Is there still value for them in a nice GUI?
Speaker 2 [34:37]
Yeah, we hope we make it accessible to non-programmers because you only have to know as little as possible. We try to make it as simple as possible for beginners to define UI.
Speaker 1 [34:54]
All right. Thank you so much. Thank you.