OMNIVERSE KIT UI STYLE BEST PRACTICE#
Overview#
This guide outlines best practices for implementing centralized styling in omni.ui, providing examples for building Kit-based applications with custom themes from the start and serving as a reference for future development.
Why Centralized Styling Matters:
Centralized styling using ui.style.default ensures consistent UI design across your application. By managing styles in one location, you gain:
consistency - new UI elements automatically use your defined colors and spacing;
maintainability - updating a value in one place changes it everywhere;
clarity - style definitions stay separate from business logic;
flexibility - switching themes or color schemes becomes straightforward. This pattern scales well as your application grows and makes it easier for teams to collaborate on UI development.
In this guideline, you will learn about the styling best practices, as well as the best practices to build window and widgets:
Styling Best Practices
✅ Use global styling with ui.style.default for application-wide styles
✅ Each extension should have its own style.py for extension-specific styles
✅ Use styling constants over magic numbers
✅ Use single centralized STYLES dictionary per extension and establish clear styles for different selectors
✅ Use name and style_type_name_override attribute instead of inline style parameter
ui.Window Best Practices
✅ Always derive from ui.Window when creating new window objects
✅ **Use kwargs to allow users to pass styling and configuration parameters
✅ Use frame.set_build_fn() or ui.Frame(build_fn=self.build) to build window content - enables lazy construction
✅ Use frame’s style or frame.set_style() to apply window-specific styling
✅ Create the window in its own dedicated file
Widget Development Best Practices
✅ For reusable widgets, encapsulate in classes - better state management and APIs
✅ **Use kwargs to allow users to pass parameters to the underlying Frame
✅ Use frame.set_build_fn() or ui.Frame(build_fn=self.build) pattern for custom widgets
Table of Contents#
Best Practice with Examples#
1. Styling Best Practices#
Criterion: Use centralized styling with proper organization - global default styles for the application and extension-specific styles for individual extensions.
1.1 Two Levels of Styling#
Global Application Styles - Set once for the entire app via
ui.style.defaultuseimport omni.ui as ui style = ui.style.default style.update(STYLES) ui.style.default = style
Extension-Specific Styles
if not defined globally, organize styles in a separate style.py file within each extension
Each extension has its own
style.pyfor extension-specific styling
Why Two Levels:
✅ Global styles ensure consistency across the entire application
✅ Extension-specific styles allow customization without polluting global namespace
✅ Not everything needs to be in the global default - extensions define their own unique styling
✅ Clear separation between app-level and extension-level styling
In your .kit file:
[dependencies]
"app.style" = {} # Load FIRST - sets global styles for the entire app
"my.extension" = {} # Extension with its own style.py for extension-specific styles
Best Practice for Global Application Styles:
# app_style_extension/styles.py
from pathlib import Path
from omni.ui import color as cl
from omni.ui import constant as fl
from omni.ui import url
import omni.ui as ui
import carb
# Define global constants
cl.app_highlight = cl("0285ED")
cl.app_background = cl("1a1a1a")
fl.border_radius = 5
url.my_icon = f"${my.extension}/icons/icon.svg"
# Global STYLES dictionary - for application-wide styling
STYLES = {
"Window": {"background_color": cl.app_background},
"Button": {"border_radius": fl.border_radius},
"Button:hovered": {"color": cl.app_highlight},
"Image::icon": {"image_url": url.my_icon},
}
# Apply as global default - this controls the ENTIRE app!
style = ui.style.default
style.update(STYLES)
ui.style.default = style
Best Practice for Extension-Specific Styles:
# my_extension/style.py
from omni.ui import color as cl
from omni.ui import constant as fl
import omni.ui as ui
# Extension-specific constants (not in global default)
cl.my_ext_primary = cl("FF5722")
fl.my_ext_spacing = 12
# Extension-specific STYLES - only for this extension
STYLES = {
"Window::my_extension": {"background_color": cl.my_ext_primary},
"Button::my_action": {"background_color": cl.my_ext_primary},
"Label::my_header": {"font_size": 18, "color": cl.my_ext_primary},
}
# In your window.py
class MyWindow(ui.Window):
def __init__(self):
super().__init__("My Window")
# Apply extension-specific styles
self.frame.set_style(STYLES)
self.frame.set_build_fn(self._build_ui)
def _build_ui(self):
...
1.2 Use styling constants#
Use constants defined from omni.ui instead of magic numbers
from omni.ui import color as cl # for colors
from omni.ui import constant as fl # for float numbers, e.g. width, height etc
from omni.ui import url # for image url
❌ WRONG:
import omni.ui as ui
# Anti-pattern: Magic numbers everywhere
ui.Label("Text", style={"color": 0xFFC0C0C0, "font_size": 14})
ui.Rectangle(style={"background_color": 0xFF343432, "border_radius": 5})
✅ CORRECT:
from omni.ui import color as cl
from omni.ui import constant as fl
# Define constants with meaningful names
cl.my_text_color = cl("C0C0C0")
cl.my_container_bg = cl("343432")
fl.my_font_size_medium = 14
fl.my_border_radius = 5
STYLES = {
"Label": {
"color": cl.my_text_color,
"font_size": fl.my_font_size_medium
},
"Rectangle::container": {
"background_color": cl.my_container_bg,
"border_radius": fl.my_border_radius
}
}
Why This Is Wrong:
No semantic meaning for values
Impossible to maintain consistency
Can’t implement theming or branding changes
Hard to understand what colors represent
⚠️ IMPORTANT: These are Global Singletons
The omni.ui.color, omni.ui.constant, and omni.ui.url modules are singletons shared across the entire application. This means:
All extensions share the same
omni.ui.color,omni.ui.constant, andomni.ui.urlsingleton instancesAny attribute you set (e.g.,
cl.my_color = cl("FF0000")) is visible to all extensionsName collisions between extensions will cause conflicts and overwrites
Use unique, extension-specific prefixes to avoid clashes
Best Practice for Naming:
# ✅ GOOD - Extension-specific prefixes prevent conflicts
# These set attributes on the global omni.ui.color/constant/url singletons
cl.myext_primary_color = cl("FF5722")
cl.myext_text_color = cl("C0C0C0")
fl.myext_spacing_large = 16
url.myext_icon_add = f"{ICON_PATH}/add.svg"
# ❌ BAD - Generic names that could conflict with other extensions
cl.primary = cl("FF5722") # Another extension might overwrite this!
cl.text_color = cl("C0C0C0") # Very likely to conflict
fl.spacing = 16 # Too generic - will be overwritten
url.icon = f"{ICON_PATH}/add.svg" # Will definitely conflict
1.2.1 Color Constants
There are several ways of representing colors in the styling system, such as 0xFF23211F and cl("1F2123"). We recommend using cl("1F2123") or cl("CCCCCCCC") (if Alpha channel is involved) for better readability and consistency.
Note that 0xFF23211F uses ABGR format (Alpha-Blue-Green-Red), where the red and blue color channels are reversed. When you use cl("1F2123"), it follows the more intuitive ARGB format (Alpha-Red-Green-Blue), making it easier to work with standard hex color representations.
1.2.2 Float Constants
Float constants in the styling system are not actual numeric values—they are special objects designed for the style system. While they work perfectly within style dictionaries, they cannot be used in mathematical calculations or with functions that expect real numbers.
✅ WORKS - Regular Python constants:
IMAGE_SIZE = 16
ui.Button("", image_width=IMAGE_SIZE, image_height=IMAGE_SIZE)
FONT_SIZE = 16
height = ui.Pixel(3 * FONT_SIZE) # Works fine
❌ DOES NOT WORK - Float constants in calculations:
from omni.ui import constant as fl
fl.image_size = 16.0
ui.Button("", image_width=fl.image_size, image_height=fl.image_size) # Will fail!
fl.font_size = 16.0
height = ui.Pixel(3 * fl.font_size) # Will fail!
When you try to use float constants in calculations, you’ll get an error because the system cannot convert the style constant object into the actual numeric value needed for the math operation.
Key takeaway: If you need to perform calculations with values (e.g., ui.Pixel(3 * FONT_SIZE)), use regular Python numeric constants instead of float constants.
1.2.3 URL Constants
When referencing resources like icons or images in your styles, use the extension path syntax instead of manually constructing file paths. This approach is more maintainable and resilient to directory structure changes.
✅ RECOMMENDED:
"image_url": "${omni.kit.markup.core}/icons/Close.svg"
❌ NOT RECOMMENDED:
ICON_PATH = Path(__file__).parent.parent.parent.parent.parent.joinpath("icons")
"image_url": f"{ICON_PATH}/Close.svg"
1.3 No Scattered styles#
Use a single centralized STYLES dictionary per extension and establish clear styles for different selectors. We keep all styles in one place and no scattered Style Variables
❌ WRONG:
import omni.ui as ui
# Anti-pattern: Multiple scattered style dictionaries
objects_container = {
"Rectangle":{
"background_color": editable_text_field_color,
"border_radius": 5,
}
}
object_button_style = {
"Button":{
"background_color": button_color,
"hovered_background_color": button_color,
},
"Button::attach_object:hovered": {
"background_color": cl.button_color,
},
}
# Later in code
with ui.ZStack():
ui.Rectangle(style=objects_container)
self.attach_object_button = ui.Button(
"Attach 3D Object",
image_url=f"{EXTENSION_FOLDER_PATH}/data/image_add.svg",
style=object_button_style
)
✅ CORRECT:
# In centralized styles.py
from omni.ui import color as cl
from omni.ui import constant as fl
from omni.ui import url
cl.myext_editable_text_field = cl("343432")
cl.myext_button = cl("0285ED")
fl.myext_container_border_radius = 5
url.myext_image_add = f"{ICON_PATH}/image_add.svg"
STYLES = {
"Rectangle::objects_container": {
"background_color": cl.myext_editable_text_field,
"border_radius": fl.myext_container_border_radius,
},
"Button::attach_object": {
"background_color": cl.myext_button,
"image_url": url.myext_image_add,
},
"Button::attach_object:hovered": {
"background_color": cl.myext_button,
},
}
# In code.py - clean and simple
frame.set_style(STYLES)
with frame:
with ui.ZStack():
ui.Rectangle(name="objects_container")
self.attach_object_button = ui.Button(
"Attach 3D Object",
name="attach_object"
)
Why This Is Wrong:
Styles scattered across multiple variables
Hard to maintain consistency
Difficult to implement themes or dark/light mode
Style logic mixed with widget creation logic
1.4 Use Hierarchical Selector Pattern (Type::Name:State)#
In omni.ui, styles are defined with three types of selectors: type Selector, name Selector and state Selector. They are structured as:
Type Selector :: Name Selector : State Selector
e.g.,Button::okButton:hovered
where Button is the type selector, which gets the default Button styles, okButton is the name selector, whose style overrides the default Button type’s style. hovered is the sate selector.
Example:
style = {
# Type selector - applies to all Buttons
"Button": {"border_width": 0.5, "margin": 5.0},
# Type + Name selector - specific button instances
"Button::primary": {
"background_color": cl("#097eff"),
"border_color": cl("#1d76fd"),
},
"Button::secondary": {
"background_color": cl.white,
"border_color": cl("#B1B1B1")
},
# Type + Name + State selector - interactive states
"Button::primary:hovered": {
"background_color": cl("#006eff")
},
"Button::primary:pressed": {
"background_color": cl("#6db2fa")
},
}
with ui.HStack(style=style):
ui.Button("Save", name="primary")
ui.Button("Cancel", name="secondary")
1.5 No inline styles - use name and style_type_name_override#
name of a widget is used to select an optional named variant that overrides only some values while inheriting the rest from the base style.
What if the user has a customized widget which is not a standard omni.ui one, or I just simply have too many okButton (taken previous example of Button::okButton:hovered) which I want them to have different styles. How to define that Type Selector?
style_type_name_override allows users to give more meaningful type names for example “Rectangle.DragAndDrop” instead of “Rectangle”, which brings much more ease for debugging and the cleanliness to the lengthy style dictionary.
This enables a common pattern:
style_type_name_overrideselects the base style “type” to use for a widgetnameselects an optional named variant (a more specific style key) that defines variant styles that override only specific properties.Use
style_type_name_overrideto pick the base, andnameto apply a variant on top.
Example:
STYLES = {
"RedRect": {"background_color": cl.red},
"RedRect::round_border": {"border_radius": fl.border_radius},
}
class MyWindow(ui.Window):
def __init__(self):
super().__init__("My Window")
self.frame.set_style(STYLES)
self.frame.set_build_fn(self._build_ui)
def _build_ui(self):
with ui.HStack():
ui.Rectangle(style_type_name_override="RedRect")
ui.Rectangle(style_type_name_override="RedRect", name="round_border")
What this does
ui.Rectangle(style_type_name_override="RedRect")Uses the style entry keyed by"RedRect"→ setsbackground_colorto red.ui.Rectangle(style_type_name_override="RedRect", name="round_border")Uses"RedRect"as the base style and applies the named variant"round_border"on top → it keeps the red background from"RedRect"and additionally appliesborder_radius.
Note that :: or : is not allowed in the attribute of style_type_name_override, we can define style_type_name_override=Button.Playbar, but not style_type_name_override=Button::Playbar. If a style is defined like Button::Playbar, it is really a button without style_type_name_override, but with a name Player.
Example:
import omni.ui as ui
from omni.ui import color as cl # for colors
STYLES = {
"Button.Playbar": {"background_color": cl.red},
"Button::Playbar": {"background_color":cl.green},
}
class MyWindow(ui.Window):
def __init__(self):
super().__init__("My Window", width=800, height=600)
self.frame.set_style(STYLES)
self.frame.set_build_fn(self._build_ui)
def _build_ui(self):
with self.frame:
with ui.HStack():
ui.Button("Green button", name="Playbar") # green button
ui.Button("Red button", style_type_name_override="Button.Playbar") # red button
We should use name and style_type_name_override attribute instead of inline style parameters.
❌ WRONG:
import omni.ui as ui
from omni.ui import color as cl
str_field = ui.StringField(model, multiline=False, style={"color": cl.green})
str_field = ui.StringField(model, multiline=False, style={"color": cl.red})
str_field = ui.StringField(model, multiline=True, style={"color": cl.darkgreen})
✅ CORRECT:
import omni.ui as ui
from omni.ui import color as cl
# Define in centralized styles.py
STYLES = {
"StringField::green_field": {
"color": cl.green,
},
"StringField::red_field": {
"color": cl.red,
},
"StringField.Multiline::green_field": {
"color": cl.darkgreen,
},
}
# Use in code
str_field = ui.StringField(model, multiline=False, name="green_field")
str_field = ui.StringField(model, multiline=False, name="red_field")
str_field = ui.StringField(model, multiline=False, name="green_field", style_type_name_override="StringField.Multiline")
Why This Is Wrong:
Entangled with business logic, hard to debug - Style logic should live in style.py, not business logic
Duplication and inconsistency - Same color values repeated multiple times, easy to miss updates
No semantic meaning -
cl.greendoesn’t explain what the field represents (status? category?)Hard to maintain - Style definitions scattered throughout functional code, Changing green fields requires finding and updating every instance
Can’t reuse styles - Each widget redeclares the same styling
Breaks theming - Can’t easily switch color schemes or implement dark/light modes
1.6 Don’t miss type selector#
We always want the type selector defined e.g. “Button” or “Label” for the widget. Otherwise, the style attributes behavior could be undefined.
❌ WRONG:
ui.Label("Example 1", style={
"color" : ui.color.yellow,
"Tooltip": {"color": ui.color.black}
}, tooltip="This is a tooltip")
objects_container = {
"background_color": editable_text_field_color, # no type selector
"border_radius": 5,
}
with ui.VStack():
with ui.ZStack():
ui.Rectangle(style=objects_container)
✅ CORRECT:
STYLES = {
"Label": {
"color" : ui.color.yellow
},
"Tooltip":{
"color": ui.color.black
},
...
}
ui.Label("Example 1", style=STYLES, tooltip="This is a tooltip")
STYLES = {
"Rectangle::objects_container": {
"background_color": editable_text_field_color, # no type selector
"border_radius": 5,
},
...
}
with ui.VStack(style=STYLES):
with ui.ZStack():
ui.Rectangle(name="objects_container")
2. ui.Window Best Practices#
Criterion:
Derive from
ui.Windowand useframe.set_build_fn()for lazy UI construction.Use kwargs for ui.Window to allow customization
Best Practice:
# window.py
import omni.ui as ui
class MyWindow(ui.Window):
WINDOW_TITLE = "My Window"
def __init__(self, width=800, height=800, **kwargs):
# Pass **kwargs to allow users to customize window parameters
# (width, height, flags, dockPreference, etc.)
super().__init__(self.WINDOW_TITLE, width=width, height=height, **kwargs)
# Set build function for lazy construction
self.frame.set_build_fn(self._build_ui)
def _build_ui(self):
"""Build window content - called when window becomes visible"""
with self.frame:
with ui.VStack(spacing=15):
self._build_header()
self._build_content()
def _build_header(self):
"""Separate method for logical sections"""
...
def _build_content(self):
"""Main content area"""
...
Usage:
# Users can pass any ui.Window parameters
window = MyWindow() # Uses defaults
window = MyWindow(width=1200, height=900) # Custom size
window = MyWindow(flags=ui.WINDOW_FLAGS_NO_RESIZE) # Custom flags
window = MyWindow(dockPreference=ui.DockPreference.LEFT_BOTTOM) # Custom dock
Why This Pattern:
✅ Flexible - Users can customize window parameters without modifying the class
✅ Lazy construction - UI only built when window is visible
✅ Proper cleanup - build_fn handles destruction
✅ Clean separation of build logic
3. Widget Development Best Practices#
Criterion:
For reusable custom widgets, encapsulate them in classes using the
ui.Frame(build_fn=self.build)pattern. Classes provide better state management, lifecycle control, and reusability.Use kwargs for ui.Frame to allow customization
Recommendation: While functions can work for simple cases, classes are recommended for:
Widgets that need to maintain state
Widgets that need callbacks or public APIs
Widgets that will be reused across multiple places
Complex widgets with multiple internal components
Best Practice (Recommended for reusable widgets):
# custom_widget.py
import omni.ui as ui
class CustomComboBox:
"""Reusable combo box widget with custom styling"""
def __init__(self, label="", items=None, **kwargs):
self._label = label
self._items = items or []
self._combo_box = None
# Pass **kwargs to Frame - allows users to control Frame parameters
# (visible, width, height, style, etc.)
self.frame = ui.Frame(build_fn=self.build, **kwargs)
def build(self):
"""Build the widget UI"""
with ui.HStack():
if self._label:
ui.Label(self._label)
self._combo_box = ui.ComboBox(
0, *self._items,
style_type_name_override="CustomComboBox"
)
def get_current_value(self):
"""Public API - can't do this with functions"""
return self._combo_box.model.get_item_value_model().as_int
Usage:
# Users can pass Frame parameters via **kwargs
widget = CustomComboBox("Select:", ["A", "B", "C"]) # Default
widget = CustomComboBox("Select:", ["A", "B", "C"], width=200) # Custom width
widget = CustomComboBox("Select:", ["A", "B", "C"], visible=False) # Hidden initially
widget = CustomComboBox("Select:", ["A", "B", "C"], height=50, width=300) # Custom size
Why Classes Are Better:
✅ Flexible - **kwargs allows users to customize Frame parameters
✅ State management - store widget references, data
✅ Public API - expose methods for interaction
✅ Lifecycle control - build_fn for lazy construction
✅ Easier to test and maintain
✅ Can add callbacks and event handlers
Acceptable for simple cases:
# Simple helper function is OK for one-off UI patterns
def build_header(title):
"""Simple helper - no state needed"""
with ui.HStack(height=30):
ui.Label(title, style_type_name_override="Label.Header")
Quick Reference for Developers#
Starting a New Application?#
Set up centralized styling Create an extension for your global application style, it could contain different themes. e.g. “my_app.style”
In your .kit file:
[dependencies]
"my_app.style" = {} # Load this FIRST - it sets global styles for everything!
The Magic: Once loaded, ui.style.default controls styling for your ENTIRE application. Every window and widget automatically uses these styles.
Starting a New Window Extension?#
Step 1: Create window
# my_extension/window.py
import omni.ui as ui
class MyWindow(ui.Window):
def __init__(self):
super().__init__("My Window", width=800, height=600)
self.frame.set_build_fn(self._build_ui)
def _build_ui(self):
with self.frame:
with ui.VStack():
ui.Label("Hello", style_type_name_override="Label.Header")
Step 2: Create custom widgets
# my_extension/widgets/custom_widget.py
import omni.ui as ui
class CustomWidget:
def __init__(self, label="", **kwargs):
self._label = label
self.frame = ui.Frame(build_fn=self.build, **kwargs)
def build(self):
with ui.VStack():
ui.Label(self._label)
ui.Button("Click Me", style_type_name_override="Button.primary")
Step 3: Customize the window style Create a style.py module for the extension specific styles. And apply the STYLES dict to the window’s frame.
Checklist Before Committing#
All styles in the centralized STYLES dictionary? globally and extension wise
Applied styles with
ui.style.default = style?Included this global style extension in your kit file?
Using style.py for all the style definition for extensions
In style.py, we have one STYLES dictionary containing all the styles.
Using
color,float,urlconstants instead of magic numbers?No inline style dictionaries (
style={...})?Using
nameandstyle_type_name_overrideinstead of inline styles?Windows use
frame.set_build_fn()pattern?Reusable custom widgets use class encapsulation with Frame?
Current limitations#
In kit 109, not all extensions comply with this best practice yet. We are working on updating our extensions to comply with these guidelines. If you have extensions which are forked from Kit with local modification, here is the “how-to” guide for extensions that are not yet following the best practices.
Workarounds for Extensions Not Yet Following Best Practices#
While migrating to centralized styling, you may encounter extensions that haven’t adopted the pattern yet. Here are practical workarounds:
1. Override Window Background with frame.set_style()#
When an extension’s window doesn’t respect the global default styles, override it directly on the window’s frame:
import omni.ui as ui
from omni.ui import color as cl
STYLES = {
"Window": {"background_color": cl.black},
...
}
# In your window __init__ or after super().__init__
class MyWindow(ui.Window):
def __init__(self):
super().__init__("My Window")
# Override window-specific styles that aren't in global default
self.frame.set_style(STYLES)
self.frame.set_build_fn(self._build_ui)
2. Add Missing Styles to Extension-Specific style.py#
When widgets in third-party extensions don’t have proper styles defined, create your own extension-specific style dictionary:
# my_extension/style.py
from omni.ui import color as cl
from omni.ui import constant as fl
# Define your color constants
cl.my_black = cl.black
cl.my_border_default = cl("C1C1C1")
cl.my_border_inactive = cl("BBBBBB")
cl.my_primary = cl("90035F")
fl.border_radius_small = 3
fl.border_radius_medium = 4
fl.border_radius_large = 6
fl.border_width = 1
# Define missing styles for widgets used in your extension
MY_EXTENSION_STYLES = {
# Override third-party widget styles
"TreeView.ScrollingFrame": {"background_color": cl.my_black},
"SearchField.Frame": {
"background_color": cl.my_black,
"border_radius": fl.border_radius_small,
"border_color": cl.my_border_default,
"border_width": fl.border_width,
},
"RadioButton": {
"border_radius": fl.border_radius_large,
"background_color": cl.transparent,
"border_width": fl.border_width,
"border_color": cl.my_border_inactive,
},
"RadioButton:checked": {
"background_color": cl.my_primary,
"border_color": cl.transparent,
},
"ComboBox::renderer_choice": {
"background_color": cl.my_black,
"border_radius": fl.border_radius_medium,
"border_width": fl.border_width,
"border_color": cl.my_border_inactive,
},
}
# Apply in your window
self.frame.set_style(MY_EXTENSION_STYLES)
3. Replace or Update Icons#
If extensions use old icon assets that don’t match your design system, provide updated SVG icons:
# Update icon paths in your style dictionary
STYLES = {
"Image::plus_icon": {"image_url": f"{YOUR_ICON_PATH}/Plus.svg"},
"Image::minus_icon": {"image_url": f"{YOUR_ICON_PATH}/Minus.svg"},
}
4. Adjust Widget Sizes When Needed#
Some widgets may need size adjustments to work with your custom styles:
# If default widget sizes don't work with your styling
self._option_button = OptionsButton(option_items, width=25, height=25) # was 20x20
5. Define Custom Style Constants#
For consistent theming across workarounds, define your own style constants instead of using inline style assignments:
# style.py
from omni.ui import color as cl
# Define your app's color palette
cl.app_background = cl("000000")
cl.app_border_default = cl("C1C1C1")
cl.app_border_active = cl("FFFFFF")
# Use consistently in all style overrides
STYLES = {
"Window": {"background_color": cl.app_background},
"SearchField.Frame": {"border_color": cl.app_border_default},
"SearchField.Frame:selected": {"border_color": cl.app_border_active},
}
6. Update widgets to fix the customized design#
Remove or replace the widgets that conflict with the desired design. If a built-in widget is hard to restyle to the desired design, update the widget. For example, remove the filter button from the search field when not needed.
Migration Strategy#
When working with non-compliant extensions:
Start with global defaults - Set up your
app.styleextension firstIdentify gaps - Note which widgets don’t respect global styles
Create extension-specific overrides - Use
frame.set_style()for missing stylesRefactor widgets - Refactor the widgets if they do not fit in the design.
This approach lets you maintain consistent styling across your app while waiting for all extensions to adopt the centralized styling pattern.