Textual Composition Pattern¶
netbox-cli uses a React-style composition pattern for Textual UI work: build screens from small reusable widgets, pass configuration through constructor arguments, and compose behavior by nesting widgets instead of building deep inheritance trees.
Why¶
- keeps layout readable in
compose() - makes theme and styling rules reusable
- lets small widgets evolve independently
- reduces fragile base-class coupling
- maps well to NetBox's own Python-side UI composition model
Core Rule¶
Prefer composition over inheritance for UI structure.
Use inheritance when:
- extending a Textual primitive with a narrow, reusable behavior such as
NbxButton - creating a self-contained stateful widget with a clear public API
Prefer composition when:
- assembling headers, bodies, toolbars, and content regions
- sharing visual structure across multiple screens
- expressing "slots" such as header/body/footer areas
React Mapping¶
React pattern:
<Panel>
<PanelHeader title="Object Attributes" subtitle="NetBox detail-style panel" />
<PanelBody>
<Status />
<Table />
<Trace />
</PanelBody>
</Panel>
Textual pattern in this repo:
class ObjectAttributesPanel(Vertical):
def compose(self):
yield NbxPanelHeader("Object Attributes", "NetBox detail-style panel")
with NbxPanelBody(id="detail_panel_body"):
yield Static("Ready", id="detail_status")
yield DataTable(id="detail_table")
yield Static("Cable Trace", id="detail_trace_title", classes="hidden")
yield Static("", id="detail_trace", classes="hidden")
Standard Building Blocks¶
Current shared composition primitives live in netbox_cli/ui/widgets.py:
NbxButtonNbxPanelHeaderNbxPanelBody
These should be the default starting point for new reusable UI pieces.
Guidelines¶
1. Compose screens from leaf widgets¶
Keep App.compose() focused on arranging major regions.
- app shell
- top bar
- sidebar
- main workspace
- overlays
Move repeated subtrees into dedicated widgets once they have meaning.
2. Treat constructor args like React props¶
Widget inputs should be explicit and semantic.
Good:
NbxButton("Send", size="medium", tone="primary")
NbxButton("Close", size="small", tone="error")
NbxPanelHeader("Object Attributes", "NetBox detail-style panel", tone="primary")
NbxPanelBody(surface="background")
Avoid passing styling intent indirectly through ad-hoc class strings when a semantic argument would be clearer.
2.1 Theme values should also be props¶
Theme-aware reusable widgets should receive semantic styling inputs through constructor arguments, similar to React props.
Preferred:
NbxButton("Send", size="medium", tone="primary")
NbxPanelHeader("Danger Zone", tone="error")
NbxPanelBody(surface="panel")
Avoid:
Use semantic props such as:
sizetonesurfacechrome
3. Use nested widgets as slots¶
When a widget has recognizable regions, model them as child widgets instead of one large monolith.
- header
- body
- footer
- toolbar
- empty state
4. Keep public methods behavior-focused¶
A composed widget should expose intent-level methods such as:
set_loading()set_object()set_trace()
Avoid leaking internal child structure unless the caller truly owns that structure.
5. Keep styling in TCSS¶
Composition defines structure. TCSS defines appearance.
- use semantic classes on reusable widgets
- keep theme logic in TCSS and theme JSON
- avoid runtime color decisions in widget constructors
6. Keep inheritance shallow¶
Do not create long widget inheritance chains for layout reuse.
Preferred:
ObjectAttributesPanel(Vertical)composed fromNbxPanelHeaderandNbxPanelBody
Avoid:
BasePanel -> PanelCard -> DetailPanel -> ObjectAttributesPanel -> SpecializedPanel
Project-Wide Standard¶
For new Textual work in netbox-cli:
- Start with composition.
- Pass theme/styling intent as semantic props on reusable widgets.
- Extract reusable visual primitives into
netbox_cli/ui/widgets.py. - Document new primitives in contributor docs if they become project-standard.
- Only add inheritance when the widget is truly a behavior-specialized primitive.