
I always try to look for Python UI libraries that is easy to use. I have seen NiceGUI and it seemed pretty interesting, however could never get myself to try it. So, finally I thought of trying something easy but still uses some basic user interfaces. I can always build more complicated applications later if this succeeds.
The application I planned to build is a VMRS viewer. For people who are not familiar with VMRS or Vehicle Maintenance Reporting Standards, let me give a brief overview of that. VMRS is a comprehensive system designed to enhance the maintenance management processes for fleets and OEMs (Original Equipment Manufacturers), particularly in transportation and logistics industries. It provides a set of standardized codes that can be used across industries to track repairs, maintenance history, expenses, warranties across their entire fleet.
Components are classified across three different codes. Specifically Code 31, also called System codes; Code 32 or Assembly codes and finally Code 33 or Part code. Between these three all components of a vehicle are defined. For example System Code 028 (Auxiliary Transmission), Assembly Code 003 (Main Drive) and Part Code 001 defines Main Drive Assembly. These can be used to effectively report and categorize all maintenance related items.
A separate code is used for specific Repairs. For example a code of 145 indicates Brake Pad Replacement.
Project Plan
The design in this case will be classic North, South, East, West and Center layout. On West panel we place the Code 31 and Code 32 as drop down items. We will put Code 33 in Center panel as a list. When any record is clicked, the full value will be displayed on the East panel.
We will need data for the VMRS for this project. So, I searched for this data online. I downloaded a data file that has all the data from Institute for Transportation Research and Education. The file data converted to CSV looks as following,
3-Digit,6-Digit,9-Digit,Description
001,001-000,001-000-000,"AIR CONDITIONING, HEATING & VENTILATING SYSTEM"
001,001-001,001-001-000,AIR CONDITIONING ASSEMBLY - COMPLETE
001,001-001,001-001-001,AIR CONDITIONING ASSEMBLY
001,001-001,001-001-002,COMPRESSOR - AIR CONDITIONING
001,001-001,001-001-003,CYLINDER ASSEMBLY - COMPRESSOR
001,001-001,001-001-004,CYLINDER & SHAFT ASSEMBLY - COMPRESSOR
001,001-001,001-001-005,BEARING - COMPRESSOR SHAFT
001,001-001,001-001-006,"PISTON - COMPRESSOR, HVAC SYSTEM"
We will create a custom data structure for storing the VMRS data in-memory.
ROOT
|- SYSTEM (Code 31 data)
| |- ASSEMBLY (Code 32 data)
| | |- PART (Code 33 data)
| | |- PART
| |- ASSEMBLY
| | |- PART
|- SYSTEM
| |- ASSEMBLY
When application initiates, we will load all these data in memory. Let’s build this first.
VMRS Data Loader
Firstly, we will define a data structure to store this record.
"""
Basic Node for storing data for VMRS tables
It's still a modified Trie without the complexity
"""
class VmrsNode:
def __init__(self):
self.children = dict()
self.code = None
self.desc = NoneNow that we have the data structure, let’s create a data store to keep this data.
class VmrsDs:
def __init__(self):
self.root = VmrsNode()
self.rec_count = 0
"""
Private Method that splits Code 33 data in SYSTEM:ASSEMBY:PART values
"""
def _parse_key(self, value: str):
parts = (value or "").split("-", 2)
# Ensure three string components
while len(parts) < 3:
parts += ("000",)
# Return both string tokens and integer flags (0 means absent)
sx, sy, sz = parts
try:
ix = int(sx)
except Exception:
ix = 0
try:
iy = int(sy)
except Exception:
iy = 0
try:
iz = int(sz)
except Exception:
iz = 0
return (sx, sy, sz), (ix, iy, iz)
"""
Insert a new Record
"""
def insert(self, key: str, value: str):
(sx, sy, sz), (ix, iy, iz) = self._parse_key(key)
if ix == 0:
return
# Increment Process count
self.rec_count += 1
node_x = self.root.children.get(sx)
if node_x is None:
node_x = VmrsNode()
node_x.code = key
node_x.desc = value
self.root.children[sx] = node_x
if iy == 0:
return
node_y = node_x.children.get(sy)
if node_y is None:
node_y = VmrsNode()
node_y.code = key
node_y.desc = value
node_x.children[sy] = node_y
if iz == 0:
return
node_z = node_y.children.get(sz)
if node_z is None:
node_z = VmrsNode()
node_z.code = key
node_z.desc = value
node_y.children[sz] = node_zThis code gets a value from the spreadsheet, parses the record, breaks it into respective components and stores it into an in-memory data structure. There are other methods used for retrieving data, but I am skipping it from this blog.
Since this blog is about NiceGUI, I will start on that class now.
GUI implementation
As described before, I have the UI setup in five sections. The code looks like below.
def base_page(self):
# CENTER PAGE
with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')
with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4').props('width=350'):
pass
with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered').props('width=350') as right_drawer:
pass
with ui.footer().style('background-color: #3874c8'):
pass
This small piece of code will generate the five sections.

See the image above. The base block creates Section #3. #1 is the left_drawer, #2 is header. #4 is right_drawer and #5 is the footer from above code. Having got this base completed, it is just a matter of adding corresponding components in each box.
Let’s start by defining the base class.
"""
This is the main UI file
"""
class VmView(object):
def __init__(self, filename):
self.columns = [
{"name": "sysCd", "field": "sysCd", "label": "System Code", "sortable": False},
{"name": "assyCd", "field": "assyCd", "label": "Assembly Code", "sortable": False},
{"name": "partCd", "field": "partCd", "label": "Part Code", "sortable": True},
{"name": "partDesc", "field": "partDesc", "label": "Part Description", "sortable": False, 'align': 'left'},
]
vm = VmrsLoad()
self.vdb = vm.load_file(filename)
print(f"Records processed: {self.vdb.get_records_processed()}")
self.on_create()
def on_create(self):
self.base_page()
ui.run()
def base_page(self):
self.center_stage()
with ui.header(elevated=True).style('background-color: #3874c8').classes('items-center justify-between'):
ui.markdown('## Vehicle Maintenance Reporting Standards (VMRS)')
ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')
with ui.left_drawer(top_corner=True, bottom_corner=True).style('background-color: #d7e3f4').props('width=350'):
self.left_drawer()
with ui.right_drawer(fixed=False).style('background-color: #ebf1fa').props('bordered').props('width=350') as right_drawer:
self.right_drawer()
with ui.footer().style('background-color: #3874c8'):
ui.label('© 2025 suvcodes.com')
The program starts execution on line #20 when we called ui.run(). It renders all components as HTML and starts a FastAPI server. A page is displayed on the default browser. The code above shows text already added for header and footer.
We can add markdown text that converts to equivalent HTML codes. See line #25 for an example of adding a markdown text. The event is defined as below.
ui.button(on_click=lambda: right_drawer.toggle(), icon='menu').props('flat color=white')
This is an inbuilt function, but it shows how you can add an Event. Every component defines its own set of events. For example while a button defines ‘click’ event, a table may expose ‘rowClick’ event. You can also define custom events using JavaScript.
Now let’s start defining the rest of the functions.
Left Drawer code
def left_drawer(self):
with ui.column():
ui.image('../images/truck.png').classes('w-full')
with ui.grid(columns=2):
ui.label('System')
self.sel_sys = ui.select(self._load_system_data())
ui.label("Assembly")
self.sel_assy = ui.select({})
btnList = ui.button('Refresh Table').classes('w-full')
self.sel_sys.on_value_change(
lambda e: self._load_assembly_data(e.value)
)
btnList.on_click(
lambda: self._load_part_data()
)
self.sel_sys.value = '001'
This is the code for the left drawer. What it does is create three different combo boxes viz. System codes, Assembly codes and Part codes. When you select a System code, it will automatically populate the relevant Assembly codes. So on and so forth for the Part code.
It sets up an image (line #3) and two different combo boxes or selects (line #6 and line #9). The layout is columnar, so components will be laid one after the other vertically. If we wanted to stack horizontally, we will have to do ui.row() instead.
When system combo loads, we populate with the system codes. This is how that part of the code looks like.
def _load_system_data(self):
return self.vdb.get_system_values()This is calling following method from the datasource.
"""
Get SYSTEM list
"""
def get_system_values(self):
dct = {}
for key, val in self.root.children.items():
dct[val.code[:3]] = f"{val.code[:3]}: {val.desc}"
return dctWe also define an event on_value_change on line #13. This in turn will load assembly code values. The code for this part call the data source and populates the drop down item.
def _load_assembly_data(self, val):
assyD_val = self.vdb.get_assembly_values(val)
self.sel_assy.set_options(assyD_val)
if len(assyD_val) > 0:
self.sel_assy.value = list(assyD_val)[0]
self.sel_assy.update()
ui.notify('Assembly select Refreshed...')Assembly code in data source looks very similar to what we have for system. Finally on line #17 a click event is defined on the button to populate the the parts based on system and assembly data.
def _load_part_data(self):
sys_val = self.sel_sys.value
assy_val = self.sel_assy.value
part_list = self.vdb.get_parts_values(assy_val)
lst_tab = []
for key, value in part_list.items():
dct = {}
dct['sysCd'] = sys_val
dct['sysTxt'] = self.sel_sys.options[sys_val]
dct['assyCd'] = assy_val
dct['assyTxt'] = self.sel_assy.options[assy_val]
dct['partCd'] = key
dct['partDesc'] = value
lst_tab.append(dct)
self.table.rows = lst_tab Again, I am skipping the method for data source. But it is similar to the one that I had defined for system.
Right Drawer code
def right_drawer(self):
with ui.column():
with ui.grid(columns=2):
ui.label('Code 31')
self.lbl_sys = ui.label('SYSTEM').style('font-weight: 600')
self.txt_sys = ui.textarea()
with ui.grid(columns=2):
ui.label('Code 32')
self.lbl_assy = ui.label('ASSEMBLY').style('font-weight: 600')
self.txt_assy = ui.textarea()
with ui.grid(columns=2):
ui.label('Code 33')
self.lbl_part = ui.label('PART').style('font-weight: 600')
self.txt_part = ui.textarea()
This is the code for the right drawer. It creates three text areas to display details for the selected item in the center table. Again, this one is also columnar layout and items are all stacked vertically. There is no event associated with this.
Center Area
def center_stage(self):
with ui.card().style("max-width: 1024px; margin: 20px auto;"):
ui.label("All Parts").classes("text-h6")
self.table = ui.table(columns=self.columns, rows=[], row_key="partCd", selection='single')
self.table.on_select(
lambda e: self._pop_details(e)
)
This is the center area where we have a table that is populated based on system and assembly codes. We have a selection event that will populate the right side of the screen.
Since we already have all the values, populating the right side text areas is just a matter of setting them as follows.
def _pop_details(self, val):
sel = val.selection[0]
self.lbl_sys.set_text(sel['sysCd'])
self.lbl_assy.set_text(sel['assyCd'])
self.lbl_part.set_text(sel['partCd'])
self.txt_sys.set_value(sel['sysTxt'].split(': ')[1])
self.txt_assy.set_value(sel['assyTxt'].split(': ')[1])
self.txt_part.set_value(sel['partDesc'].split(': ')[1])That should now have all the features that we decided to create.
Conclusion
Overall I found NiceGUI pretty easy to implement. All APIs are well documented and intuitive. There are advanced options that may take some getting used to, but building a basic application tends to be pretty fast. Hope you find this blog useful. Ciao for now!