Widget catalog
Start with the job the interface must do. Every published name below links to
its generated signature. Match that reference to the version in your
pubspec.lock.
TextInput has a curated page with its properties,
key handling, and ownership rules.
Layout and composition
| Need | Start with | Important distinction |
|---|---|---|
| Horizontal or vertical children | Row, Column, Flex | spacing inserts whole cells between children |
| One child owns leftover space | Expanded, Flexible | Expanded fills its share; Flexible may use less |
| Padding, alignment, or exact extent | Padding, Align, SizedBox | Keep each relationship in the widget that owns it |
| Minimum or maximum size | ConstrainedBox | A constraint is a range, not a promised size |
| Themed titled region | Panel | Shrink-wraps content; width and height set exact extents |
| Fill, border, and child composition | Container, DecoratedBox | Use Container for composition; DecoratedBox only paints |
| Overlapping children | Stack, Positioned | Positions are integer cell offsets |
| Children that continue onto another run | Wrap | Use Row when wrapping would be surprising |
| A child that must read its own space | LayoutBuilder | Builds during layout from the offered BoxConstraints |
LayoutBuilder reports the space a parent offers. It is available in Noir
0.0.1; see Installation.
Read Layout in terminal cells before you fix widths across a screen. Terminal proportions make one row of spacing visually larger than one column.
Text, status, and feedback
| Need | Widget | Notes |
|---|---|---|
| Plain or styled text | Text, RichText, TextSpan | Width follows terminal graphemes, not Dart string length |
| Large glyph heading | AsciiFont | Uses Noir’s own terminal-safe font families |
| Compact state label | Badge | Variants provide semantic defaults through Theme |
| Completion fraction | ProgressBar | Width is explicit and resolves to eighth-cell steps |
| Ongoing work | Spinner | Remove it when work ends; use SpinnerFrames.line for ASCII-only output |
| Section rule | Divider | Prefer one structural divider to borders around every child |
| Shared visual values | Theme | Read values with Theme.of(context) |
Use a ProgressBar for measurable work and a Spinner when progress is unknown:
Row(
spacing: 1,
children: [
if (totalBytes == null)
const Spinner(frames: SpinnerFrames.line)
else
ProgressBar(value: downloadedBytes / totalBytes, width: 24),
Text(status),
],
)Input and selection
| Need | Widget | Interaction model |
|---|---|---|
| Activate one command | Button | Enter, Space, or primary pointer activation |
| Boolean value | Checkbox, Switch | Use the label that best describes the resulting state |
| Value on a range | Slider | Arrow-key and pointer adjustment |
| One-line editing | TextInput | Enter submits; default maxLength is 100 |
| Attached suggestions | Autocomplete<T> | Caller controls options, status, selection, and async work |
| Multi-line editing | TextArea | Fixed by default; opt into wrapping, bounded growth, or Enter-to-submit |
| Vertical option list | Select | Arrows or j/k change highlight; Enter confirms |
| Horizontal section switcher | TabSelect | Fixed-width tabs by default; tabWidth: null sizes to labels |
| Local raw key ownership | Focus, FocusScope | Return ignored for keys the region does not own |
| Semantic key commands | Shortcuts, Actions | Map keys to Intents, then Intents to application commands |
| Pointer hit region | PointerListener | Read MouseEvent.localPosition after hit testing |
PointerListener.mouseCursor selects a custom mouse shape. Interactive
controls provide hover pointers automatically. See
Show a hand over an action for
requirements and behavior.
Select and TabSelect share typed options but express different spatial
models:
Select<String>(
options: const [
SelectOption(name: 'Development', value: 'dev'),
SelectOption(name: 'Production', value: 'prod'),
],
height: 2,
autofocus: true,
onSelect: (_, option) {
final environment = option.value;
if (environment != null) applyEnvironment(environment);
},
)Select.height is visible rows and defaults to 8. Set it to the option count
for a short list. SelectOption.value is nullable.
Autocomplete<T> keeps request policy outside the widget. Supply an explicit
AutocompleteStatus and options from the application. Tab enters a ready list,
Enter or a primary click selects, and Escape dismisses the attached surface
while idle Escape stays available to an ancestor.
See Handle input and focus for event order, controller ownership, and focus traversal.
Scrolling and data
| Need | Widget | Choose it when… |
|---|---|---|
| One oversized child | ScrollBox | The content already exists as one widget subtree; tail follow is opt-in |
| Repeated visible rows | ListView | Rows should be built only for the visible window |
| Hierarchical rows | TreeView | Stable caller IDs need expansion and keyboard navigation |
| Interactive dataset | DataTable | Rows need aligned columns, selection, and caller-owned sorting |
| Finite rich-text grid | TextTable | Cells need wrapping, borders, and row-major text selection |
DataTable virtualizes its body and reports sort requests; your data owner
does the reordering. TextTable lays out a finite matrix of InlineSpan cells
and supports document-style selection. They solve different table problems.
TreeViewController owns generic roots, expansion, and selection; path,
filesystem, or other domain behavior stays in the application. TreeView uses
Up and Down for visible selection, Right and Left for hierarchy navigation, and
one Enter or primary-click activation route.
Overlays and menus
| Need | Widget | Boundary |
|---|---|---|
| Custom transient content | OverlayPortal | Caller owns placement, input, focus, and dismissal |
| Centered blocking interaction | Modal | Closed focus loop and pointer barrier; caller supplies chrome |
| Commands anchored to a launcher | MenuAnchor | Follows launcher geometry and owns outside-click dismissal |
Modal keeps its ordinary child mounted, centers a fresh modalBuilder
subtree while open, loops Tab and Shift+Tab over live modal controls, restores
the captured focus node on close, and blocks every outside pointer event.
Escape dismisses by default; outside primary-button dismissal is opt-in.
Compose a Panel inside the builder, because Modal deliberately has no visual
style of its own.
final modal = ModalController();
Modal(
controller: modal,
initialFocusNode: cancelFocus,
modalBuilder: (context) => Panel(
title: 'Discard changes?',
child: Row(
spacing: 1,
children: [
Button(
label: 'Cancel',
focusNode: cancelFocus,
onPressed: modal.close,
),
Button(
label: 'Discard',
onPressed: () {
discardChanges();
modal.close();
},
),
],
),
),
child: Button(label: 'Open', onPressed: modal.open),
)One ModalController attaches to one live Modal. open() throws while
detached, close() is a detached no-op, and replacing the controller preserves
an open subtree without transition callbacks. Calling open() again promotes
that modal and reestablishes focus inside it. When a top modal closes, the
modal revealed below preserves a valid restored focus or repairs to an
available descendant.
Noir has one framework-owned root overlay. It does not expose public
OverlayEntry, nested overlays, transforms, linked layers, menu cascades, or
overlay animation APIs.
Documents and media
| Need | Widget or type | Boundary |
|---|---|---|
| Selectable source | CodeView | Plain text by default; supply a CodeHighlighter for syntax ranges |
| Unified or split patch | DiffView, UnifiedDiffParser | Caller chooses view mode and owns any source action |
| GitHub-flavoured Markdown | MarkdownView | Standalone mode scrolls; embedded mode delegates scrolling to a parent |
| Terminal image | Image, TerminalImage | File, network, encoded bytes, or RGBA; protocol support depends on the terminal |
| Semantic terminal link | linked TextSpan | The terminal decides whether and how the link is presented |
| Explicit selected-text copy | SelectedText and document callbacks | OSC52 acceptance depends on terminal policy |
For a read-only DiffView embedded in a decision surface, set
canRequestFocus: false so the enclosing modal keeps keyboard traversal.
A standalone Markdown document owns focus, selection, and its scrolling viewport:
MarkdownView(
markdown: releaseNotes,
autofocus: true,
)Check Platform support before you depend on terminal images, semantic links, or OSC52 clipboard behavior.
State and motion
StatefulWidget and its State are the normal retained state boundary.
ValueNotifier and ChangeNotifier separate testable state from one widget.
AnimationController with TickerProviderStateMixin or
SingleTickerProviderStateMixin drives time-based values. ThemeData carries
the shared visual values.
The optional companion package noir_signals offers the same ownership model
through composable hooks. Read
State, identity, and ownership to choose, and
Manage state and resources when one lifecycle concern should be
reusable.