Skip to content

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:

  • NbxButton
  • NbxPanelHeader
  • NbxPanelBody

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:

Button("Send", classes="custom-primary custom-medium")
Static("Danger Zone", classes="red-header")

Use semantic props such as:

  • size
  • tone
  • surface
  • chrome

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 from NbxPanelHeader and NbxPanelBody

Avoid:

  • BasePanel -> PanelCard -> DetailPanel -> ObjectAttributesPanel -> SpecializedPanel

Project-Wide Standard

For new Textual work in netbox-cli:

  1. Start with composition.
  2. Pass theme/styling intent as semantic props on reusable widgets.
  3. Extract reusable visual primitives into netbox_cli/ui/widgets.py.
  4. Document new primitives in contributor docs if they become project-standard.
  5. Only add inheritance when the widget is truly a behavior-specialized primitive.