Layout & Container Widgets
Scrolling Model
Several widgets support scrolling. Understand the two modes before choosing:
Uncontrolled (default)
Do not set an explicit scroll offset property. The runtime manages internal scroll state on mouse wheel, key scrolling, and scrollbar drag.
ScrollView::new()
.scrollbar(true) // Draggable scrollbar out of the box
.child(long_content)Controlled
Set an explicit scroll offset property. The parent is the source of truth:
ScrollView::new()
.offset(self.scroll) // Controlled by parent state
.on_scroll_to(ctx.link().callback(Msg::Scrolled)) // Update parent state
.child(content)Scroll Callbacks
| Callback | Emits | Used by |
|---|---|---|
on_scroll | ScrollEvent { offset, metrics } | ScrollView, TextArea |
on_scroll_to | usize (target offset) | ScrollView, TextArea, List, Table |
VStack / HStack
Vertical/Horizontal stack containers. Default sizing: width: Flex(1), height: Flex(1).
| Prop | Type | Description |
|---|---|---|
gap | u16 | Space between children |
padding | impl Into<Padding> | Inner padding |
align | Align | Cross-axis alignment |
justify | Justify | Main-axis packing |
style | Style | Container style |
border | bool | Draw border |
border_style | BorderStyle | Border appearance |
focus_policy | FocusPolicy | Accordion behavior (includes sticky: bool, default true) |
tab_titles | Vec<String> | Border-embedded tab titles |
active_tab | usize | Active border tab index |
width | Length | Width override |
height | Length | Height override |
Accordion focus policy:
VStack::new()
.focus_policy(FocusPolicy::Accordion(FocusAccordion {
focused_min: 10,
collapsed: 1,
..FocusAccordion::default()
}))
.child(frame_a.key("a"))
.child(frame_b.key("b"))The accordion automatically remembers the last focused child and keeps it expanded when focus moves outside the stack (sticky: true by default) - see focus.md.
Flow
Wrapping layout container for chip/tag-like content. Flow packs children left-to-right and automatically continues on the next row when a child does not fit the remaining width.
| Prop | Type | Description |
|---|---|---|
gap | u16 | Space between children on both axes |
align | Align | Cross-axis alignment for items inside each wrapped row |
padding | Padding | Inner padding around the content area |
border | bool | Draw a border around the container |
border_style | BorderStyle | Border style variant |
children | Vec<Element> | Child elements to place in flow order |
style | Style | Container style |
width | Length | Width override |
height | Length | Height override |
Flow::new()
.gap(1)
.align(Align::Start)
.children(vec![
Text::new("rust").style(Style::new().bg(Color::Blue).fg(Color::Black).bold()).into(),
Text::new("tui-lipan").style(Style::new().bg(Color::Cyan).fg(Color::Black).bold()).into(),
Text::new("layout").style(Style::new().bg(Color::Magenta).fg(Color::Black).bold()).into(),
])Use Flow for mixed-width chips, badges, and quick filters where the number of items is dynamic and row breaks must adapt to container resizing.
ZStack
Overlay container - children stack on top of each other.
| Prop | Type | Description |
|---|---|---|
style | Style | Container style |
passthrough | bool | Allow pointer events through non-interactive layers |
width | Length | Width |
height | Length | Height |
Frame
Container with border, title, optional status line, and tab affordances.
| Prop | Type | Description |
|---|---|---|
title | impl Into<String> | Frame title |
title_style | Style | Title style |
focus_title_style | Style | Title style when focused |
title_align | Align | Title alignment |
status | impl Into<String> | Right-side status text |
status_style | Style | Status text style |
focus_status_style | Style | Status style when focused |
border | bool | Draw border |
border_style | BorderStyle | Border appearance |
border_merge_mode | BorderMergeMode | Replace | Exact | Fuzzy (default Exact) |
join_frame | bool | Draw junction caps when adjacent to another bordered Frame |
active_tab | usize | Active border tab index |
tab_titles | Vec<String> | Border-embedded tab titles |
active_tab_style | Style | Active tab style |
inactive_tab_style | Style | Inactive tab style |
focus_active_tab_style | Style | Active tab when frame focused |
focus_inactive_tab_style | Style | Inactive tab when frame focused |
compact | bool | Single-line mode |
decoration | FrameDecoration | Single edge overlay |
decorations | Vec<FrameDecoration> | Multiple edge overlays |
padding | impl Into<Padding> | Inner padding |
style | Style | Container style |
width | Length | Width (default Flex(1)) |
height | Length | Height (default Flex(1)) |
Clipping: Children are automatically clipped to the Frame's inner content area (inside borders and padding).
Edge decorations: With border: false, DecorationPlacement::Border still draws on the frame body edge; the layout engine reserves those cells so children (and full-width list selection) do not paint over the decoration band.
Integrated scrollbars (ScrollbarVariant::Integrated) treat those bands like a drawn border: a right or left Border decoration is the vertical track; bottom or top is the horizontal track (e.g. TextArea with an integrated horizontal scrollbar inside a borderless framed panel).
Grid
Explicit row/column tracks with Length (Auto / Px / Percent / Flex), independent horizontal and vertical gaps, row-major auto-flow for .child(…), and .cell / .cell_span for explicit placement. Auto tracks size to their contents; use Flex tracks when you want columns or rows to absorb remaining parent space.
| Prop | Type | Description |
|---|---|---|
columns | [Length] | Column track list (default one Auto column if omitted) |
rows | [Length] | Row track list (default one Auto row if omitted) |
gap | u16 | Sets both gap_x and gap_y |
gap_x / column_gap | u16 | Horizontal gap between columns |
gap_y / row_gap | u16 | Vertical gap between rows |
uniform_columns(n) | usize | Shorthand for n× Length::Auto columns |
padding | Padding | Inner padding |
align / justify | Align / Justify | Child alignment within each cell |
width / height | Length | Requested size |
border / border_style | … | Optional border |
Grid::new()
.columns([Length::Px(20), Length::Flex(1), Length::Auto])
.rows([Length::Auto, Length::Flex(1)])
.gap_x(1)
.gap_y(0)
.child(Text::new("auto-placed"))
.cell(1, 2, Text::new("explicit"))
.cell_span(0, 0, 1, 3, Text::new("span"))
// Builder order for span on the last auto-placed child:
Grid::new()
.child(Text::new("wide"))
.span(2, 1)See examples/grid_basic.rs.
ScrollView
Scrollable container with optional scrollbar.
| Prop | Type | Description |
|---|---|---|
offset | Option<usize> | Controlled scroll offset |
scroll_request | Option<ScrollRequest> | One-shot relative scroll request (lines, page fractions, top, bottom) |
scroll_to_key | Option<Key> | Scroll to the first child subtree containing this key |
scroll_keys | ScrollKeymap | Configure keyboard scroll keys |
scroll_wheel | bool | Enable mouse wheel scrolling |
ambient_page_scroll | bool | Opt this ScrollView into PageUp/PageDown fallback routing when no focused handler or on_key scope handles the key |
focusable | bool | Whether ScrollView is focusable |
scrollbar | bool | Show vertical scrollbar |
scrollbar_config | ScrollbarConfig | Full scrollbar configuration (variant, gap, thumb, thumb styles) |
show_scroll_indicators | bool | Show top/bottom overflow indicators |
scroll_indicator_style | Style | Overflow indicator style |
estimated_child_height | u16 | Cold-start fallback height for unmeasured off-screen children (default 3) |
on_scroll | Callback<ScrollEvent> | Scroll event (includes metrics) |
on_scroll_to | Callback<usize> | Target offset |
width | Length | Width |
height | Length | Height |
scrollbar_config: Use ScrollbarConfig to configure scrollbar appearance beyond the on/off toggle. It has its own builder methods:
ScrollView::new()
.scrollbar_config(
ScrollbarConfig::new()
.enabled(true)
.variant(ScrollbarVariant::Integrated)
.thumb('▐')
.thumb_style(Style::new().fg(Color::DarkGray))
.thumb_focus_style(Style::new().fg(Color::Cyan))
.gap(1)
)The same ScrollbarConfig type is used on all scrollable widgets (List, Table, TextArea, DocumentView, etc.).
Clipping: Children are automatically clipped to ScrollView's inner viewport.
One-shot scroll requests: Use .scroll_request(...) for command-driven moves without permanently controlling the settled offset.
ScrollView::new()
.scroll_request(ScrollRequest::half_page_down())For custom fractions, use ScrollRequest::viewport_fraction(numerator, denominator). Positive values move down; negative values move up.
Priority: scroll_to_key(...) overrides scroll_request(...), which overrides offset(...).
Ambient page scroll fallback: Use .ambient_page_scroll(true) when you want PageUp / PageDown to target one explicit ScrollView even if it is not focused. This fallback runs only after normal focused-widget dispatch, ancestor scroll bubbling, and component on_key bubbling all decline the key. To avoid ambiguity, ambient page scroll activates only when exactly one mounted ScrollView has the flag set.
Controlled tail alignment: If you keep passing a tail-style controlled offset (for example usize::MAX or another value that stays at/beyond the current max offset), ScrollView stays bottom-pinned even when content grows from fully fitting the viewport to becoming scrollable on a later layout pass. If you want growth to keep the viewport at the top instead, do not pass a tail-aligned offset for those frames.
Stable key + tail: When the same logical timeline may be reparented (e.g. full-width vs HStack + sidebar), give the ScrollView a stable key as the last builder step (.key("…") on the IntoElement chain). The runtime records whether that key was at the scroll bottom last frame and restores tail-pinch after node-id churn, without width probes.
Center
Centers a single child both horizontally and vertically within the available area. The child is sized to its natural (minimum) dimensions; it does not expand to fill the container.
Note:
Centerremains useful because it is more semantic and concise than the equivalentVStack::new().align(Align::Center).justify(Justify::Center)pattern, and it guarantees the child is sized naturally rather than proportionally.
Center::new().child(my_widget)| Prop | Type | Description |
|---|---|---|
style | Style | Container style |
width | Size | Override centered width (Auto, Fixed, Percent) |
height | Size | Override centered height |
CenterPin
Pins one child to the true center of the container. The remaining space is split equally above and below, and given to top and bottom children respectively. Those zones are collision-aware: they never overlap the pinned child regardless of how their content changes.
This is the right widget when you need one element always at the exact middle of the screen while other content (headers, status bars, navigation, etc.) can be added or removed dynamically.
CenterPin::new()
.top(VStack::new().child(header).child(nav))
.center(dialog_or_textarea)
.bottom(status_bar)| Prop | Type | Description |
|---|---|---|
top | impl Into<Element> | Element placed in the zone above the center child |
center | impl Into<Element> | Element always pinned to the true center |
bottom | impl Into<Element> | Element placed in the zone below the center child |
style | Style | Container style (e.g. background) |
Sizing: defaults to Flex(1) on both axes - it fills its parent.
Layout algorithm:
- Measure the
centerchild to determine its height. - Place the center child at
(total_h − center_h) / 2from the top. - Give everything above that position to
top, everything below tobottom.
The top and bottom zones receive only what remains, so a taller center child naturally compresses both zones symmetrically.
MouseRegion
Wraps any subtree to handle pointer movement, clicks, and hover visuals.
| Prop | Type | Description |
|---|---|---|
on_click | Callback<MouseEvent> | Emits on left-button click (MouseKind::Down(Left)) |
on_mouse_move | Callback<MouseMoveEvent> | Emits on pointer movement |
capture_click | bool | If true, captures left-clicks before interactive children |
hover_style | Style | Pre-paint style applied while hovered; best for backgrounds and modifiers |
hover_effect / hover_effects | VisualEffect / iterator | Post-process rendered child content while hovered |
hover_dim / hover_lighten / hover_tint | f32 / Color, f32 | Convenience post-processing effects; hover_tint affects both fg and bg |
enabled | bool | Toggle move/click handling and hover behavior |
MouseRegion::new()
.on_click(ctx.link().callback(|e: MouseEvent| Msg::Click(e.x, e.y)))
.capture_click(true)
.on_mouse_move(ctx.link().callback(|e: MouseMoveEvent| {
Msg::Hover { x: e.local_x, y: e.local_y }
}))
.hover_style(Style::new().bg(Color::AnsiValue(236)))
.child(my_widget)hover_style and hover_effect are intentionally different layers. hover_style paints before the wrapped child subtree renders, so it works well for hover backgrounds and modifiers but may not recolor child text foregrounds that the child paints afterward. To recolor rendered text, use hover_effect with a foreground-only transform:
MouseRegion::new()
.hover_effect(VisualEffect::transform_fg(ColorTransform::Tint(theme.text, 1.0)))
.child(my_widget)hover_tint(color, alpha) is a symmetric tint shortcut and blends both foreground and background toward color. At alpha = 1.0, both channels become that color.
MouseMoveEvent fields: x, y (terminal-space), local_x, local_y (relative to MouseRegion rect), target_w, target_h, mods.
Mouse motion processing is only active when at least one move listener is present in the tree.
capture_click(true)only reroutes left-click handling when this region has anon_clickcallback.
EffectScope
Wraps any subtree and post-processes the rendered cells inside its bounds.
Use it when you want to dim an inactive pane, tint a whole section, quantize a subtree to a retro palette, or animate a composed ZStack after it has already rendered.
| Prop | Type | Description |
|---|---|---|
style | Style | Effect style; use render-time effects like dim_by, lighten_by, tint_by, transform_fg, transform_bg, or contrast_policy |
effect | VisualEffect | Append one declarative post-processing effect |
effects | IntoIterator<Item = VisualEffect> | Append multiple effects in declaration order |
EffectScope::new()
.dim_by(0.35)
.child(sidebar)
EffectScope::new()
.effect(VisualEffect::Monochrome { strength: 0.8 })
.effect(VisualEffect::Scanlines {
strength: 0.25,
spacing: 2,
})
.effect(VisualEffect::RainbowWave {
blend: 0.5,
frequency: 1.3,
speed: 1.0,
axis: EffectAxis::Diagonal,
})
.child(content)Effects are applied in insertion order. Nested EffectScopes compose naturally: the inner scope post-processes first, then the outer scope applies its own pass over the already-composed result.
EffectScope affects the final rendered subtree, so explicit child colors are still transformed. Direct replacement colors like .fg(...) and .bg(...) are not used to repaint the subtree.
Built-in VisualEffect variants:
| Effect | Description |
|---|---|
Dim { amount } | Dim fg/bg colors after render |
Tint { color, alpha } | Blend subtree colors toward a tint |
Monochrome { strength } | Desaturate toward grayscale |
PaletteQuantize { palette } | Snap colors to a small palette |
Scanlines { strength, spacing } | Dim every Nth row |
RainbowWave { blend, frequency, speed, axis } | Animated per-cell color wave |
Gradient { gradient, blend, frequency, speed, axis } | Sine-eased mirrored ColorGradient sampled along axis; nested scopes remap independently |
RetroCrt { preset, flicker, scanline_strength } | Preset built from simpler primitives |
Clipped { bounds, mask, inner } | Clip / mask an inner effect (see effects.md) |
EffectPalette presets: Cga, Gameboy, Amber, Green, Custom(Vec<Color>).
RetroPreset presets: Amber, Green, Cga, Gameboy, VaultTec.
Spacer
Flexible empty space. Expands to fill available space in a stack.
HStack::new()
.child(left_content)
.child(Spacer::new()) // Pushes right_content to the end
.child(right_content)Divider
Visual separator line.
| Prop | Type | Description |
|---|---|---|
orientation | Orientation | Constructor - Horizontal or Vertical |
style | Style | Divider style |
ch | char | Line glyph character |
label | Element | Label (horizontal only) |
label_alignment | Align | Label position along divider |
label_padding | u16 | Padding around label |
join_frame | bool | Draw junction caps when inside a bordered Frame |
Splitter
Resizable container with draggable handles between panes.
| Prop | Type | Description |
|---|---|---|
orientation | - | Use Splitter::horizontal() (top/bottom) or Splitter::vertical() (left/right) |
weights | Vec<u16> | Initial weight for each pane |
min_size | Vec<u16> | Minimum size for each pane in cells |
handle_size | u16 | Handle gutter width/height |
handle_symbol | char | Handle character |
handle_style | Style | Handle idle style |
handle_hover_style | Style | Handle hover style |
handle_active_style | Style | Handle drag style |
join_frame | bool | Overlay handles onto shared pane seams |
width | Length | Width |
height | Length | Height |
Splitter::vertical() // Left/Right split
.weights(vec![30, 70])
.min_size(vec![10, 20])
.child(sidebar)
.child(main_content)Frame-join mode: set join_frame(true) alongside neighboring Frame::join_frame(true) panes so the merged border itself becomes the splitter handle (no extra gutter).
Animated
Wrapper for opacity and/or height transitions. height sets the animation target; stacks measure that value for layout and gap math.
| Prop | Type | Description |
|---|---|---|
opacity | f32 | Target opacity (0.0…1.0) |
opacity_target | Option<Color> | When Some, opacity blends fg (and bg unless opacity_fg_only) toward this color instead of the terminal/theme backdrop; target changes snap (not animated) |
opacity_fg_only | bool | When true, opacity post-pass affects foreground only (backgrounds stay solid; use behind fixed panel fills) |
fg | Option<Color> | Target foreground color; lerps to the target using transition timing |
bg | Option<Color> | Target background color; lerps to the target using transition timing |
height | Length | Target height (Auto, Px, …) |
layout_height | Option<Length> | When Some, used for stack measurement instead of height (keep Some(Length::Auto) while collapsing so gap stays stable, then None after on_height_transition_end) |
on_opacity_transition_end | Callback<()> | Fires once when an opacity transition reaches its target (including zero-duration jumps) |
on_height_transition_end | Callback<()> | Fires once when a height transition reaches its target (including zero-duration jumps) |
transition | TransitionConfig | Duration and easing |
opacity applies a post-pass alpha transform that blends rendered fg/bg toward the terminal background by default (unless opacity_fg_only or opacity_target is set). With opacity_target, fades go to a chosen color (fade-to-black, flash-to-accent) instead of the host backdrop. fg and bg are explicit color targets that lerp with the same transition timing. You can combine them (for example, fade + tint) in one Animated wrapper.
For correct opacity blending when backgrounds use Color::Reset, set App::terminal_bg(query_host_colors().map(|c| c.bg)) before run() - see quick-start.md (terminal_bg / query_host_colors).