Skip to content

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.

rust
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:

rust
ScrollView::new()
    .offset(self.scroll)               // Controlled by parent state
    .on_scroll_to(ctx.link().callback(Msg::Scrolled))  // Update parent state
    .child(content)

Scroll Callbacks

CallbackEmitsUsed by
on_scrollScrollEvent { offset, metrics }ScrollView, TextArea
on_scroll_tousize (target offset)ScrollView, TextArea, List, Table

VStack / HStack

Vertical/Horizontal stack containers. Default sizing: width: Flex(1), height: Flex(1).

PropTypeDescription
gapu16Space between children
paddingimpl Into<Padding>Inner padding
alignAlignCross-axis alignment
justifyJustifyMain-axis packing
styleStyleContainer style
borderboolDraw border
border_styleBorderStyleBorder appearance
focus_policyFocusPolicyAccordion behavior (includes sticky: bool, default true)
tab_titlesVec<String>Border-embedded tab titles
active_tabusizeActive border tab index
widthLengthWidth override
heightLengthHeight override

Accordion focus policy:

rust
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.

PropTypeDescription
gapu16Space between children on both axes
alignAlignCross-axis alignment for items inside each wrapped row
paddingPaddingInner padding around the content area
borderboolDraw a border around the container
border_styleBorderStyleBorder style variant
childrenVec<Element>Child elements to place in flow order
styleStyleContainer style
widthLengthWidth override
heightLengthHeight override
rust
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.

PropTypeDescription
styleStyleContainer style
passthroughboolAllow pointer events through non-interactive layers
widthLengthWidth
heightLengthHeight

Frame

Container with border, title, optional status line, and tab affordances.

PropTypeDescription
titleimpl Into<String>Frame title
title_styleStyleTitle style
focus_title_styleStyleTitle style when focused
title_alignAlignTitle alignment
statusimpl Into<String>Right-side status text
status_styleStyleStatus text style
focus_status_styleStyleStatus style when focused
borderboolDraw border
border_styleBorderStyleBorder appearance
border_merge_modeBorderMergeModeReplace | Exact | Fuzzy (default Exact)
join_frameboolDraw junction caps when adjacent to another bordered Frame
active_tabusizeActive border tab index
tab_titlesVec<String>Border-embedded tab titles
active_tab_styleStyleActive tab style
inactive_tab_styleStyleInactive tab style
focus_active_tab_styleStyleActive tab when frame focused
focus_inactive_tab_styleStyleInactive tab when frame focused
compactboolSingle-line mode
decorationFrameDecorationSingle edge overlay
decorationsVec<FrameDecoration>Multiple edge overlays
paddingimpl Into<Padding>Inner padding
styleStyleContainer style
widthLengthWidth (default Flex(1))
heightLengthHeight (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.

PropTypeDescription
columns[Length]Column track list (default one Auto column if omitted)
rows[Length]Row track list (default one Auto row if omitted)
gapu16Sets both gap_x and gap_y
gap_x / column_gapu16Horizontal gap between columns
gap_y / row_gapu16Vertical gap between rows
uniform_columns(n)usizeShorthand for n× Length::Auto columns
paddingPaddingInner padding
align / justifyAlign / JustifyChild alignment within each cell
width / heightLengthRequested size
border / border_styleOptional border
rust
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.

PropTypeDescription
offsetOption<usize>Controlled scroll offset
scroll_requestOption<ScrollRequest>One-shot relative scroll request (lines, page fractions, top, bottom)
scroll_to_keyOption<Key>Scroll to the first child subtree containing this key
scroll_keysScrollKeymapConfigure keyboard scroll keys
scroll_wheelboolEnable mouse wheel scrolling
ambient_page_scrollboolOpt this ScrollView into PageUp/PageDown fallback routing when no focused handler or on_key scope handles the key
focusableboolWhether ScrollView is focusable
scrollbarboolShow vertical scrollbar
scrollbar_configScrollbarConfigFull scrollbar configuration (variant, gap, thumb, thumb styles)
show_scroll_indicatorsboolShow top/bottom overflow indicators
scroll_indicator_styleStyleOverflow indicator style
estimated_child_heightu16Cold-start fallback height for unmeasured off-screen children (default 3)
on_scrollCallback<ScrollEvent>Scroll event (includes metrics)
on_scroll_toCallback<usize>Target offset
widthLengthWidth
heightLengthHeight

scrollbar_config: Use ScrollbarConfig to configure scrollbar appearance beyond the on/off toggle. It has its own builder methods:

rust
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.

rust
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: Center remains useful because it is more semantic and concise than the equivalent VStack::new().align(Align::Center).justify(Justify::Center) pattern, and it guarantees the child is sized naturally rather than proportionally.

rust
Center::new().child(my_widget)
PropTypeDescription
styleStyleContainer style
widthSizeOverride centered width (Auto, Fixed, Percent)
heightSizeOverride 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.

rust
CenterPin::new()
    .top(VStack::new().child(header).child(nav))
    .center(dialog_or_textarea)
    .bottom(status_bar)
PropTypeDescription
topimpl Into<Element>Element placed in the zone above the center child
centerimpl Into<Element>Element always pinned to the true center
bottomimpl Into<Element>Element placed in the zone below the center child
styleStyleContainer style (e.g. background)

Sizing: defaults to Flex(1) on both axes - it fills its parent.

Layout algorithm:

  1. Measure the center child to determine its height.
  2. Place the center child at (total_h − center_h) / 2 from the top.
  3. Give everything above that position to top, everything below to bottom.

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.

PropTypeDescription
on_clickCallback<MouseEvent>Emits on left-button click (MouseKind::Down(Left))
on_mouse_moveCallback<MouseMoveEvent>Emits on pointer movement
capture_clickboolIf true, captures left-clicks before interactive children
hover_styleStylePre-paint style applied while hovered; best for backgrounds and modifiers
hover_effect / hover_effectsVisualEffect / iteratorPost-process rendered child content while hovered
hover_dim / hover_lighten / hover_tintf32 / Color, f32Convenience post-processing effects; hover_tint affects both fg and bg
enabledboolToggle move/click handling and hover behavior
rust
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:

rust
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 an on_click callback.


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.

PropTypeDescription
styleStyleEffect style; use render-time effects like dim_by, lighten_by, tint_by, transform_fg, transform_bg, or contrast_policy
effectVisualEffectAppend one declarative post-processing effect
effectsIntoIterator<Item = VisualEffect>Append multiple effects in declaration order
rust
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:

EffectDescription
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.

rust
HStack::new()
    .child(left_content)
    .child(Spacer::new())   // Pushes right_content to the end
    .child(right_content)

Divider

Visual separator line.

PropTypeDescription
orientationOrientationConstructor - Horizontal or Vertical
styleStyleDivider style
chcharLine glyph character
labelElementLabel (horizontal only)
label_alignmentAlignLabel position along divider
label_paddingu16Padding around label
join_frameboolDraw junction caps when inside a bordered Frame

Splitter

Resizable container with draggable handles between panes.

PropTypeDescription
orientation-Use Splitter::horizontal() (top/bottom) or Splitter::vertical() (left/right)
weightsVec<u16>Initial weight for each pane
min_sizeVec<u16>Minimum size for each pane in cells
handle_sizeu16Handle gutter width/height
handle_symbolcharHandle character
handle_styleStyleHandle idle style
handle_hover_styleStyleHandle hover style
handle_active_styleStyleHandle drag style
join_frameboolOverlay handles onto shared pane seams
widthLengthWidth
heightLengthHeight
rust
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.

PropTypeDescription
opacityf32Target opacity (0.01.0)
opacity_targetOption<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_onlyboolWhen true, opacity post-pass affects foreground only (backgrounds stay solid; use behind fixed panel fills)
fgOption<Color>Target foreground color; lerps to the target using transition timing
bgOption<Color>Target background color; lerps to the target using transition timing
heightLengthTarget height (Auto, Px, …)
layout_heightOption<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_endCallback<()>Fires once when an opacity transition reaches its target (including zero-duration jumps)
on_height_transition_endCallback<()>Fires once when a height transition reaches its target (including zero-duration jumps)
transitionTransitionConfigDuration 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).

MIT OR Apache-2.0