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, DocumentView, TextArea
on_scroll_tousize (target offset)ScrollView, TextArea, List, Table
on_viewport_changeScrollViewportEventScrollView

VStack / HStack

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

Layout pitfall checklist:

  • Stacks consume remaining space by default. Use Length::Px(...) for fixed bars before giving the main content flexible space.
  • A child can end up with a zero-width or zero-height rect when the parent viewport is too small or fixed siblings consume the available space.
  • For headless debugging, set an explicit TestBackend viewport and capture with UiSnapshotOptions::diagnostic(); markdown flags zero-area widgets as zero-area.
PropTypeDescription
gapu16Space between children
paddingimpl Into<Padding>Inner padding
alignAlignCross-axis alignment
justifyJustifyMain-axis packing
styleStyleContainer style
borderboolDraw border
border_styleBorderStyleBorder appearance
focus_sizingFocusSizingAccordion sizing behavior (includes sticky: bool, default true)
focus_scopeFocusScopeSubtree traversal behavior (None, Exclude, or Contain)
tab_titlesVec<String>Border-embedded tab titles
active_tabusizeActive border tab index
active_tab_styleStyleActive border tab style
extend_active_tab_style / inherit_active_tab_styleStyle / ()Extend or inherit the selection theme role for the active border tab
widthLengthWidth override
heightLengthHeight override

Accordion focus sizing:

rust
VStack::new()
    .focus_sizing(FocusSizing::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.

Pointer hit testing

NodeTree depth-first hit testing visits VStack / HStack children in reverse document order (see depth_first_test in src/core/node/tree.rs). That means the last child in the source list is considered before earlier siblings when deciding which subtree to search first.

For normal stacked layout, each child receives a non-overlapping rectangle on the main axis, so a given (x, y) lies inside at most one child’s rect and traversal order does not change the resolved target.

If you build overlapping siblings inside the same stack (uncommon) or need visual layers that all share the parent’s full rectangle (foreground decoration over content, dock under a logo, etc.), use a ZStack. Set passthrough when misses on non-interactive foreground layers should reach the layer below. For shape-specific pointer regions instead of layered passthrough, use MouseRegion hit-test refinement, as shown in examples/burst_effects.rs.


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
row_gapu16Vertical gap between wrapped rows, independent of item gap
alignAlignCross-axis alignment for items inside each wrapped row
justifyJustifyMain-axis distribution of items within 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
shrinkableboolYield width before normal siblings, allowing item truncation under pressure
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.

justify distributes each row's leftover width independently: SpaceBetween pins the first item of every row to the left edge and the last to the right edge, Center/End shift whole rows, and SpaceAround/SpaceEvenly pad the edges too. Unlike HStack/VStack, Flow items are always measured at their natural size, so the space variants work without giving children explicit non-flex sizing:

rust
Flow::new()
    .gap(1)
    .justify(Justify::SpaceBetween)
    .children(tags)

ZStack

Overlay container - children stack on top of each other. Each child is laid out with the same bounds as the ZStack. The last child is painted on top.

PropTypeDescription
styleStyleContainer style
passthroughboolWhen true, pointer routing can fall through non-interactive layers to lower children that share the same bounds (foreground + background pattern). When false, only the topmost matching layer is traversed.
widthLengthWidth
heightLengthHeight

For decorative overlays, set ZStack::passthrough(true) so a non-interactive foreground layer does not block the content below it:

rust
ZStack::new()
    .passthrough(true)
    .child(content)
    .child(decorative_overlay)

Pointer routing still visits the painted foreground first. Interactive layers remain targets; only misses on non-interactive layers fall through.


Canvas

Absolute-positioned child container. Each child is placed at a Rect in Canvas-local coordinates, then clipped to the Canvas bounds. Children are painted in declaration order, so later children appear visually on top.

PropTypeDescription
child_at(Rect, impl Into<Element>)Add a child at a Canvas-local rectangle
itemsIntoIterator<Item = CanvasItem>Replace all positioned children
styleStyleCanvas background/effect style
passthroughboolLet pointer routing pass through non-interactive top layers
widthLengthRequested width (default Flex(1))
heightLengthRequested height (default Flex(1))
rust
Canvas::new()
    .child_at(
        Rect { x: 2, y: 1, w: 26, h: 6 },
        Frame::new().header_left("Logs").child(log_panel),
    )
    .child_at(
        Rect { x: 18, y: 4, w: 24, h: 7 },
        Frame::new().header_left("Inspector").child(inspector),
    )

Use Canvas when the application owns geometry directly: floating panes, drag previews, custom compositors, or demos that intentionally overlap children. For normal responsive layout, prefer VStack, HStack, Grid, Flow, or ZStack. For animated app-owned geometry, use ctx.transition(...) or a state-owned Transition<FloatRect>, then pass the current value’s .to_rect() to child_at(...).

Pointer routing follows painter order. With the default passthrough(false), a Canvas only descends into the topmost child whose rect contains the pointer, so overlapped regions block lower windows while exposed regions of lower windows remain clickable. Use passthrough(true) for decorative overlays that should let misses fall through to lower children.

Canvas is documented here because it is a layout primitive; see examples/window_manager.rs for a complete tiling/floating compositor-style example.


Frame

Container with border, positional header/footer labels, and tab affordances.

Border labels are organized into two independent groups. Each group has left, center, and right positions. The grouped API is canonical; the header_left/footer_right methods are thin convenience setters.

rust
Frame::new()
    .header(BorderLabels::new().left("Title").right("Status"))
    .footer(
        BorderLabels::new()
            .left("Mode")
            .center("Workspace")
            .right("Time"),
    )

For custom styles, group styles provide defaults and per-label styles extend them. Focused styles are applied only while the frame is focused:

rust
Frame::new().header(
    BorderLabels::new()
        .left(
            FrameLabel::new("Project")
                .style(Style::new().fg(Color::Cyan))
                .focused_style(Style::new().bold()),
        )
        .center("Workspace 1")
        .right("Connected")
        .style(Style::new().fg(Color::Gray))
        .focused_style(Style::new().fg(Color::White))
        .padding(1),
)
PropTypeDescription
headerBorderLabelsTop border labels (left / center / right)
footerBorderLabelsBottom border labels (left / center / right)
header_contentimpl Into<Element>Optional content row inside a borderless frame
focus_styleStyleFrame style when focused
extend_focus_style / inherit_focus_styleStyle / ()Extend or inherit the focus theme role instead of replacing it
hover_styleStyleFrame style when hovered
extend_hover_style / inherit_hover_styleStyle / ()Extend or inherit the hover theme role instead of replacing it
borderboolDraw border
border_styleBorderStyleBorder appearance
border_edgesBorderEdgesBorder geometry (All or HorizontalCaps)
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
tab_edgeTabEdgeWhich border draws the tabs: Top (default) or Bottom
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))
focus_scopeFocusScopeSubtree traversal behavior (None, Exclude, or Contain)

Border tabs: tab_titles embeds a clickable tab strip in a border line, and tab_edge picks which one. Tabs share their line with that border's own labels, so TabEdge::Bottom puts them beside footer_left / footer_right while the header labels keep the top line to themselves.

rust
Frame::new()
    .header_left("DevTools")
    .tab_titles(["Stats", "Logs", "App"])
    .tab_edge(TabEdge::Bottom)
    .active_tab(active)
    .on_tab_change(ctx.link().callback(Msg::TabChanged))
    .child(body)

Reach for bottom tabs when the frame is anchored to the bottom of its container: that edge is the one that stays put, so the strip keeps its screen position even as the body above it changes height. Top tabs on a bottom-anchored panel shift every time the content resizes. A compact frame has only one line and draws its tabs there whatever the edge says.

Clipping: Children are automatically clipped to the Frame's inner content area (inside borders and padding).

Horizontal caps: Frame::border_edges(BorderEdges::HorizontalCaps) draws the top and bottom border rows with corner caps, but does not reserve left/right columns for content. Use it for lighter panel chrome when full vertical borders feel too heavy.

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
reveal_horizontal_range(usize, usize)Minimally pan to reveal a half-open horizontal content range; reapplies when the range or viewport width changes
scroll_requestOption<ScrollRequest>One-shot relative scroll request (lines, page fractions, top, bottom)
scroll_toOption<ScrollTarget>Semantic target (Top, Bottom, Key, or KeyOffset) resolved each layout
scroll_to_keyOption<Key>Convenience wrapper for ScrollTarget::Key
scroll_to_key_offset(Key, usize)Convenience wrapper for ScrollTarget::KeyOffset, useful for landing inside large keyed rows
scroll_to_top / scroll_to_bottom-Convenience wrappers for edge targets that do not need sentinel children
scroll_behaviorScrollBehaviorInstant by default; opt into smooth target movement
scroll_transitionTransitionConfigShortcut for smooth target movement with a transition config
scroll_keysScrollKeymapConfigure keyboard scroll keys
scroll_wheelboolEnable mouse wheel scrolling
scroll_wheel_multiplieru16Override the app-wide wheel line multiplier for this ScrollView
scroll_wheel_behaviorScrollWheelBehaviorImmediate by default; opt into inertial wheel movement
smooth_wheel_scrollboolConvenience toggle for default inertial wheel physics
scroll_accelerationf32Convenience setter that enables smooth wheel scrolling and changes the wheel impulse
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 vertical scrollbar configuration (variant, gap, thumb, thumb styles)
axisScrollAxisScroll axes: Vertical (default), Horizontal, or Both
h_scrollbarboolShow horizontal scrollbar when axis includes horizontal scrolling
h_scrollbar_configScrollbarConfigHorizontal scrollbar styling (same type as scrollbar_config)
h_scroll_wheel_multiplieru16Horizontal wheel step (columns); falls back to scroll_wheel_multiplier, then the app default
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
on_viewport_changeCallback<ScrollViewportEvent>Fires when the visible immediate-child snapshot changes after layout/reconcile
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.

Horizontal scrolling: Use .axis(ScrollAxis::Both) (or ScrollAxis::Horizontal) with .h_scrollbar(true) when content is wider than the viewport. Children measure to their natural width instead of stretching to the viewport width. Arrow Left/Right and shift+mouse wheel pan horizontally; Up/Down and the vertical wheel remain on the vertical axis when both are enabled.

With ScrollAxis::Horizontal the plain mouse wheel also pans horizontally, since there is no vertical axis to disambiguate against. Once the view reaches the relevant edge the tick is left unhandled and bubbles to an ancestor, so a horizontal strip nested inside a vertical ScrollView does not trap the wheel. Shift+wheel keeps working as the explicit horizontal override on every axis configuration.

rust
ScrollView::new()
    .axis(ScrollAxis::Both)
    .scrollbar(true)
    .h_scrollbar(true)
    .child(wide_and_tall_content)

See examples/scroll_view_both_axes.rs for a combined vertical + horizontal demo.

Horizontal range reveal: Use .reveal_horizontal_range(start, end) to keep a half-open content-column range visible without controlling the horizontal offset directly. The view pans only far enough to expose the range and leaves an already-visible range in place. The request is reevaluated when the range or viewport width changes; user wheel, key, or scrollbar movement remains authoritative while both values stay unchanged.

rust
ScrollView::new()
    .axis(ScrollAxis::Horizontal)
    .reveal_horizontal_range(match_start, match_end)
    .child(wide_content)

Viewport visibility: .on_viewport_change(...) reports which immediate ScrollView children are visible, entered, or exited. It fires after layout/reconcile when that snapshot changes, including resize, wrapping, content changes, and insertion/removal - not only user scroll. Put stable keys on the immediate row children for reliable diffs across insertion/removal; descendants are not tracked individually. Reported content_rect, viewport_rect, and visible_rect values are framework Rects: content-relative before offset, effective-viewport-relative after offset/indicator rows, and clipped visible portion respectively.

Performance note: on_viewport_change can fire during fast wheel scrolling or scrollbar dragging. If the callback only mirrors event.offset into parent state, return Update::none() from your component update handler. Use Update::layout() when visible-child metadata changes the emitting component's view or mounted subtree, such as a sticky header. Use Update::full() only when the change affects other scopes or root-level composition.

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: target scrolling (scroll_to(...), scroll_to_key(...), scroll_to_key_offset(...), scroll_to_top(), scroll_to_bottom()) is framework-owned and persistent, but fresh one-shot scroll_request(...) values or explicit controlled offset(...) changes can interrupt and suppress the current target until the target changes.

Smooth target scrolling: .scroll_behavior(ScrollBehavior::smooth_default()), .scroll_behavior(ScrollBehavior::smooth_adaptive()), or .scroll_transition(config) animates semantic targets from scroll_to(...), scroll_to_key(...), scroll_to_key_offset(...), scroll_to_top(), and scroll_to_bottom(). Adaptive timing derives the transition duration from the resolved row distance and caps long jumps. Controlled .offset(...), scroll_request(...), key scrolling, scrollbar drag, and default mouse wheel scrolling remain immediate. User input cancels any active smooth target animation.

Smooth wheel scrolling: .smooth_wheel_scroll(true) opts mouse wheel input into inertial row movement. The default remains immediate for compatibility. Use .scroll_wheel_behavior(ScrollWheelBehavior::smooth(config)) for full physics control or .scroll_acceleration(value) for the common acceleration tweak.

rust
ScrollView::new()
    .smooth_wheel_scroll(true)
    .scroll_acceleration(56.0)

Terminal backends expose discrete wheel up/down events rather than pixel-level trackpad deltas. Smooth wheel scrolling treats those line events as velocity impulses, so it can move farther than immediate mode for the same wheel input, especially when events are repeated or coalesced. Use the default immediate mode when each wheel step must map to an exact row delta.

Use .scroll_wheel_multiplier(lines) when one ScrollView should scroll a different number of lines per wheel tick than the app-wide App::scroll_wheel_multiplier(...) setting.

For scroll-to-edge flows, prefer built-in edge targets over invisible sentinel children:

rust
ScrollView::new()
    .scroll_to_bottom()
    .scroll_behavior(ScrollBehavior::smooth_adaptive())

Edge targets resolve against the actual content extent on each layout pass, so they continue following top/bottom as content height changes and do not affect layout or gap(...) spacing.

For precise message-list jumps, place the Key on the immediate ScrollView child representing the message. Nested keys still use the existing behavior: the view scrolls to the containing top-level child, not the exact descendant row. Use scroll_to_key_offset(key, rows) when a large keyed row contains an auto-height child and navigation should land inside that row. For example, a timeline can render one auto-height DiffView per file, key each file card, and scroll to file_card_top + hunk_logical_row for global hunk navigation.

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.


PanView

Single-child two-dimensional viewport for wide/tall content such as diagrams.

PropTypeDescription
childimpl Into<Element>Content rendered at its natural size
offset(i32, i32)Controlled (x, y) pan offset; negative offsets are allowed when unclamped
on_panCallback<PanEvent>Fired after drag or keyboard panning changes the offset
clampboolClamp offsets to content bounds (default true)
center_contentboolStart uncontrolled views centered until input or remembered state takes over (default false)
free_pan_marginu16With clamp(false), limit movement so at least this many cells of content remain reachable
free_pan_margins(u16, u16)Independent horizontal/vertical free-pan margins
drag_to_panboolEnable left-button drag panning (default true)
wheel_to_panboolEnable mouse-wheel panning (default true)
keymap / pan_keysPanKeymapKeyboard pan keys (ARROWS, VIM, default both)
key_step(u16, u16)Keyboard pan step as (horizontal, vertical) cells (default (4, 2))
focusableboolWhether PanView can receive focus (default false; interaction callbacks can opt it in)
tab_stopboolInclude in sequential Tab traversal (default: true)
on_focus / on_blurCallback<()>Focus gained / lost
pan_state_keyimpl Into<Key>Stable key for uncontrolled offset persistence
widthLengthViewport width
heightLengthViewport height
rust
PanView::new()
    .child(diagram)
    .width(Length::Flex(1))
    .height(Length::Px(20))
    .clamp(false)
    .center_content(true)
    .free_pan_margin(2)
    .key_step((4, 2))
    .pan_state_key("diagram-preview")

Dragging right/down decreases the offset; dragging left/up increases it. Keyboard panning defaults to a wider horizontal step than vertical step because terminal cells are taller than they are wide.

Wheel panning: the mouse wheel pans vertically and shift+wheel horizontally, each tick moving one key_step in that axis, so wheel and keyboard panning share one step size and the horizontal step stays wider for the same reason. With the default clamp(true), a tick that cannot move the viewport is left unhandled and bubbles to an ancestor, so a PanView sitting inside a scrolling page releases the wheel once it reaches its edge. An unclamped free canvas has no edge and therefore keeps consuming the wheel, which is what an infinite surface should do; bound it with .free_pan_margin(...) if you want it to release. Set .wheel_to_pan(false) to opt out entirely. With the default clamp(true), offsets stay within 0..=max for each axis. Use .clamp(false) for free-canvas previews where the child can be pulled past the viewport edges; add .free_pan_margin(...) when you want that free movement bounded instead of infinite. The child rect is translated by the negative offset and clipped to the viewport, so child hit-testing stays aligned with visible content.

When the image feature is enabled, input-driven PanView movement temporarily renders image children as lightweight placeholder frames while movement stabilizes. This mirrors image handling during layout changes and keeps pan interaction responsive for expensive image protocols.


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
on_drag_start / on_drag / on_drag_endCallback<MouseDragEvent>Left-button drag lifecycle after threshold
drag_requires_modsKeyModsRequire modifiers before left-button drag callbacks can start
drag_thresholdu16, u16Pointer travel in cells before drag callbacks start; default (3, 1)
on_right_drag_start / on_right_drag / on_right_drag_endCallback<MouseDragEvent>Right-button drag lifecycle after threshold
right_drag_requires_modsKeyModsRequire modifiers before right-button drag callbacks can start
bubble_mouse_downboolAlso emit on_mouse_down for descendant presses without consuming them
capture_clickboolIf true, captures left-clicks before interactive children
capture_requires_modsKeyModsCapture pointer handling over descendants while modifiers are held
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)

Use bubble_mouse_down(true) for container focus policies where the container must learn about descendant presses but the child should still receive its click. Use drag_requires_mods(KeyMods::ALT) or right_drag_requires_mods(KeyMods::ALT) for compositor-style gestures that should only start while Alt is held. A drag starts once the pointer travels DEFAULT_DRAG_THRESHOLD — 3 columns or 1 row — from the press, which keeps a jittery click from selecting text or picking up a list item. Columns are looser because a cell is about twice as tall as it is wide. A region with no click gesture to disambiguate from — a resize handle, a split divider, a slider thumb — should set drag_threshold(1, 1) so the pointer is tracked from its first step; otherwise a horizontal gesture stalls for two cells and then arrives three cells out in one jump. Pair those with capture_requires_mods(KeyMods::ALT) when the wrapped child is a terminal or text widget, so Alt-click/Alt-drag is fully consumed by the wrapper.

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-button handling when this region has a left-click, left-down/up, or left-drag callback. Right-drag callbacks are resolved through the target's ancestor chain and do not require capture_click. capture_requires_mods(...) applies the same rerouting only while the required modifiers are held, and prevents terminal mouse forwarding for those events.


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.

Root-portal descendants, including default Modal overlays, inherit ancestor EffectScopes. You can wrap the portal element itself or a container that contains it; the overlay content is post-processed at its rendered portal bounds. If the wrapped child is just a component shell around portal content, only the rendered portal content is affected, not the declaration-site backdrop area.

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
Ripple { origin, radius, ring_width, tint, strength }Aspect-correct radial ring; radius: RippleRadius can be Fixed, Loop, or Once, and origin can be explicit cells (EffectOrigin::cell) or aligned from current scope bounds (EffectOrigin::aligned)
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. Perpendicular dividers using the standard light or heavy line glyphs automatically compose junctions where they meet, including corners, T-junctions, and crossings. Custom ch glyphs keep last-write-wins rendering.

PropTypeDescription
orientationOrientationConstructor - Horizontal or Vertical
styleStyleDivider style
chcharLine glyph character
labelElementLabel (horizontal only)
label_alignmentAlignLabel position along divider
label_padding / label_padding_axesu16 / (u16, u16)Left/right inset of a horizontal label (default 1). Inset cells still show the divider character - only the label itself clears the line - so a left inset of 1 yields ─title rather than a blank. Use label_padding_axes when the sides should differ.
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
handle_modeSplitterHandleModeGutter (default) or Border (ride the pane border seam)
widthLengthWidth
heightLengthHeight
rust
Splitter::vertical()   // Left/Right split
    .weights(vec![30, 70])
    .min_size(vec![10, 20])
    .child(sidebar)
    .child(main_content)

Handle mode: handle_mode(SplitterHandleMode::Border) drops the gutter and rides the pane border seam instead of drawing its own handle glyph. Thickness follows the borders actually present, so this is independent of whether the neighboring Frames merge their borders:

  • panes whose Frame::join_frame(true) borders merge share one wall → a 1-cell handle on it,
  • panes that keep separate borders expose two adjacent walls → a 2-cell handle grabbing both,
  • borderless panes fall back to a synthetic 1-cell handle on the seam.

Note Frame::join_frame (border merging) is a separate, still-current visual choice owned by the frames.

Corner drag: when a vertical and a horizontal splitter handle meet (nested splitters), clicking on or next to the junction grabs both handles at once - dragging moves the seam on both axes simultaneously, like a tiling window manager corner. This is automatic; no opt-in.


Animated

Wrapper for opacity, fg/bg color, height, and opt-in x/y position 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)
position_transitionboolEnables FLIP-style visual x/y movement when the same keyed Animated node receives a new final layout rect
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)
on_position_transition_endCallback<()>Fires once when a position transition reaches its final layout position (including zero-duration jumps)
transitionTransitionConfigDuration and easing
exit(bool, u64)Sets opacity, animated height, and duration for an app-driven appear/disappear
on_exit_completeCallback<()>Alias for on_height_transition_end; the safe point to drop the item from state
auto_exitimpl Into<ExitAnimation>Animates out and drops the element automatically when a keyed child is removed from its container. A bare u64 means "fade over this many ms"

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). At opacity(0.0), the wrapper restores the cells that were already rendered underneath it, so fully faded content does not leave invisible glyphs or blank cells blocking lower ZStack/overlay layers. 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).

Exit transitions

There are two ways to animate an element out. Prefer auto_exit unless the application genuinely needs to own the lifecycle.

Automatic (auto_exit)

Add .auto_exit(..) and a key to an Animated wrapper sitting directly in a VStack, HStack, ZStack, or Canvas. Removing the element from your state is then all you have to do: the container keeps the already-rendered subtree, animates it out, and drops it. There is no removed flag and no completion callback to wire up.

rust,ignore
VStack::new().children(
    ctx.state.rows.iter().map(|row| {
        Animated::new(render_row(row)).auto_exit(180).key(row.id)
    }),
)

The key is what tells the container that a specific child left rather than that the list reordered, so it is required. Nothing is cloned: the container keeps the existing node it already reconciled.

Saying what leaving looks like

auto_exit takes anything that converts into an ExitAnimation. A bare duration is the shorthand for a plain fade; the full type describes the exit:

rust,ignore
// A toast: slide up a row, fade, and let the toasts below close the gap.
Animated::new(toast)
    .auto_exit(ExitAnimation::slide(180, 0, -1).with_collapse(true))
    .key(id)

// Flash before going.
Animated::new(row)
    .auto_exit(ExitAnimation::new(220).bg(theme.status.error))
    .key(id)

// Carried entirely by movement.
Animated::new(chip)
    .auto_exit(ExitAnimation::slide(140, 8, 0).keep_opacity())
    .key(id)
MethodEffect
ExitAnimation::new(ms)Fade to transparent
ExitAnimation::slide(ms, dx, dy)Fade while translating dx columns, dy rows
ExitAnimation::collapse(ms)Fade while collapsing to zero height
.opacity(v) / .keep_opacity()Fade to v, or leave opacity alone
.fg(c) / .bg(c)Animate colors on the way out
.offset(dx, dy)Translate while leaving
.easing(e)Override the widget's own transition easing
.with_collapse(bool)Collapse height as well

Unset properties are left alone. auto_exit never changes how the element looks or lays out while it is alive: the exit carries its own duration and easing, so setting one does not disturb the widget's transition.

offset is relative to the position the element occupied when it was removed, not to its layout rectangle. If a movement transition was still in flight at that moment, the exit starts from wherever the element had actually reached and absorbs the remainder, so a row removed mid-reorder slides on from where it was rather than snapping back first.

What the container decides

Everything visual comes from the ExitAnimation. The one thing it does not control is whether height collapses, because that is a layout question the parent owns.

A VStack or HStack always collapses on its main axis, whatever the exit says. The container substitutes a spacer of the collapsing height (VStack) or width (HStack) at the child's former index, so siblings reflow exactly as they would if you had kept describing the element. The collapse starts from the size the child actually occupied on screen, not from any layout prop you set.

A Canvas or ZStack collapses only if the exit asked for it with .with_collapse(true). These children carry their own rectangle and nothing reflows around them, so there is no space to reclaim and the collapse is a pure effect. It often reads better anyway, an element that recedes is easier to follow than one that dissolves in place, but see the ceiling below before reaching for it on a bordered box.

The two positioned containers differ in depth. A ZStack is explicit layering, so an exiting layer keeps the depth it had and fades over whatever is beneath it. A Canvas places children in space, where overlap is incidental and a ghost covering live content is always wrong, so exiting children are drawn beneath every live one. That is what stops a closing pane from covering the neighbour expanding into its space.

The ceiling

The retained subtree is frozen. The container keeps the node it already reconciled and nothing is cloned, re-derived, or re-laid out, which is what makes retention nearly free. It is a hard boundary, not a missing feature.

So an exit can change how the already-rendered cells look: alpha, color, where they are painted, and how much of the element is visible. It can never change where the element's children sit. There is no scale, no reflow, no re-wrap. A collapsing bordered box does not squash to fit: it is clipped, and the bottom border is cut away on the first frame rather than travelling up with it.

The rule for choosing:

  • Does the exit only change how already-rendered cells look? Use auto_exit.
  • Does it change where the element's children sit? Keep describing the element yourself, with Animated::exit and ExitQueue.
Lifecycle and disposal

A retained subtree is a snapshot, not a living element. The element stopped being described, so on that same frame its component state, hooks, command registrations, and scroll state were all disposed by the ordinary sweep. Only the resolved node data survives, which is exactly enough to keep painting it.

The framework enforces what follows, so an exit cannot reach into a dropped scope:

  • The subtree is inert: skipped for hit-testing, focus, and key routing. It cannot be clicked, cannot take focus, and receives no keys.
  • Transition-end callbacks (on_opacity_transition_end and friends) do not fire during an automatic exit.
  • Nothing re-runs view(), so no effect, command, or state read happens on its behalf.

Release waits on the transitions the exit itself started, not on "nothing is animating". Unrelated animation still settling when the element was removed keeps ticking and keeps painting, but does not extend retention past the exit it has nothing to do with. Retention also ends on a deadline derived from the exit duration, so a container that stops being rendered part-way through cannot hold the subtree forever. Re-adding the same key before the exit finishes cancels it and hands the live element back.

Unsupported containers

Grid, Flow, and ScrollView have no automatic retention, and what a collapse should mean there is genuinely ambiguous: whether a grid re-packs around the hole, whether a wrapping flow should re-wrap on every frame of the animation, and how a scroll offset should be anchored while content height changes. Rather than guess, auto_exit under an unsupported container logs a debug-build warning and behaves as if it were not set; use Animated::exit with ExitQueue there.

Manual (exit + ExitQueue)

Use this when the application must own the lifecycle, for example when an item migrates between two collections while leaving, or when removal has to wait on work of its own.

Use Animated::exit to fade and collapse an item that is still present in component state. Animated::on_exit_complete fires after the height reaches zero, which is the safe point to remove that item from state:

rust,ignore
    Animated::new(row)
        .exit(false, 180)
        // Keep a non-zero parent allocation while the wrapper collapses.
        .layout_height(Some(Length::Auto))
        .on_exit_complete(ctx.link().callback(move |_| Msg::ExitComplete(id)))

For lists, keep an ExitQueue<Key, Row> field in component state and synchronize it with the live rows. Render each iter() entry, using the boolean to choose between the live and exiting presentation. Handle ExitComplete(id) with finish(&id). Re-adding an id before completion automatically resurrects its existing entry instead of inserting a duplicate. This state-owned recipe keeps keyed identity stable without runtime zombie keep-alive:

rust,ignore
// State: `items: Vec<Row>`, `exiting: ExitQueue<Id, Row>`
ctx.state.exiting.sync(
    ctx.state.items.iter().map(|row| (row.id, row.clone())),
);
for (id, row, visible) in ctx.state.exiting.iter() {
    let leaving = !visible;
    render_row(id, row, leaving);
}
// In Msg::ExitComplete(id):
ctx.state.exiting.finish(&id);

Keep a stable key for each exiting row and render the queued row with .exit(false, duration) until its completion callback arrives.

Exit animations only advance while their host is rendered, so a queue living in a hidden tab or an inactive workspace never receives its completion callback. Construct it with ExitQueue::with_exit_timeout(duration) so those entries are released anyway; set the timeout slightly longer than the animation. Repeated sync calls do not push an existing deadline forward.

When an item moves between collections mid-exit, hand it over with transfer_out / adopt rather than re-inserting it, which would restart its exit:

rust,ignore
if let Some(moved) = ctx.state.from.transfer_out(&id) {
    ctx.state.to.adopt(moved);
}

Position transitions

Enable .position_transition(true) to animate a wrapper from its previous screen position to its new layout position when layout changes:

rust
Animated::new(card)
    .position_transition(true)
    .transition(transition)
    .key("card-42")

Position transitions use FLIP semantics: reconciliation computes the new final layout immediately, then rendering applies a temporary visual offset that eases back to zero. This is visual-only and paint-only; it does not progressively mutate layout, measurement, focus order, scroll math, or pointer geometry.

The Animated wrapper must keep a stable .key(...) across reorders or reparenting within the same reconciled branch. Without a stable key, the runtime treats the moved item as a new node and the first mount snaps to its final position instead of animating. This is not a global shared-element system: it animates the same keyed Animated node's own rect changes, not unmount/remount matches across unrelated branches.

Hit testing, hover, drag/drop local coordinates, scrollbar zones, and focus traversal use the final node.rect for the whole transition. A card may be visibly between two positions, but clicks are recognized at the destination layout position, not at the temporary visual offset.

Clipping remains anchored in parent layout space. A moving child inside a ScrollView or Frame is still clipped to that parent viewport, while the Animated wrapper's own child clip follows the moving visual box. Opacity and color post-passes use the same visual rect as the moved subtree, so fades/tints continue to cover the rendered cells during motion.

MPL-2.0