Skip to Content
ftui-layoutResponsive breakpoints

Responsive Layouts

Terminals come in wildly different widths. The sidebar that reads well at 160 columns is suffocating at 60. ftui-layout’s responsive layer provides CSS-style breakpoints and two helper types — Responsive<T> and ResponsiveLayout — for swapping configurations at those boundaries.

Breakpoints

Breakpoint is ordered from smallest to largest and maps to a column range:

BreakpointDefault min widthTypical use
Xs< 60 colsMinimal / ultra-narrow
Sm60–89 colsCompact layouts
Md90–119 colsStandard terminal width
Lg120–159 colsWide terminals
Xl160+ colsUltra-wide / tiled

The thresholds are configurable via Breakpoints { sm, md, lg, xl }; Xs implicitly starts at width 0.

0 60 90 120 160 ┼──── Xs ──┼──── Sm ──┼──── Md ──┼──── Lg ──┼──── Xl ─▶

Responsive<T> — values by breakpoint

Responsive<T> holds a base value and optional overrides per breakpoint:

use ftui_layout::{Responsive, Breakpoint}; let columns: Responsive<u16> = Responsive::new(2) .with(Breakpoint::Md, 3) .with(Breakpoint::Lg, 4) .with(Breakpoint::Xl, 5); assert_eq!(columns.resolve(Breakpoint::Xs), &2); assert_eq!(columns.resolve(Breakpoint::Md), &3); assert_eq!(columns.resolve(Breakpoint::Xl), &5);

It works for any Tu16 for widths, bool for “show-sidebar?”, a whole theme struct, anything.

ResponsiveLayoutFlex swapping

The common case is “use this Flex when it’s narrow, this other one when it’s wide.” ResponsiveLayout is the purpose-built helper:

responsive_shell.rs
use ftui_layout::{Breakpoint, Constraint, Flex, ResponsiveLayout}; use ftui_render::Rect; // Narrow: single column. Wide: sidebar + main. Ultra-wide: sidebar + main // + inspector. let layout = ResponsiveLayout::new( // base (Xs) — single column Flex::vertical().constraints([Constraint::Fill]), ) .at( Breakpoint::Md, Flex::horizontal().constraints([ Constraint::Fixed(30), // sidebar Constraint::Fill, // main ]), ) .at( Breakpoint::Lg, Flex::horizontal().constraints([ Constraint::Fixed(30), // sidebar Constraint::Fill, // main Constraint::Fixed(40), // inspector ]), ); let area = Rect::new(0, 0, 140, 40); let split = layout.split(area); // Automatically classifies area.width into Lg and solves that Flex. assert_eq!(split.rects.len(), 3);

Important methods:

layout.at(bp, flex) // override the Flex at a breakpoint layout.split(area) // classify by width, then solve layout.split_for(bp, area) // override classification, e.g. for tests layout.classify(width) // which breakpoint owns this width? layout.layout_for(bp) // peek the Flex used for `bp` layout.detect_transition(old_width, new_width) // did we cross a boundary?

detect_transition is the hook for animating between layouts or firing telemetry when the UX shape changes.

When to use Responsive<T> vs ResponsiveLayout

Same shape, different sizes → Responsive<u16>

You always have a sidebar-plus-main split. Only the sidebar width changes with the terminal. One Flex, constraints computed with Responsive<u16>.

Different shapes entirely → ResponsiveLayout

At Xs you hide the sidebar. At Md you show it. At Lg you add an inspector column. The number of rects and their meaning changes, so the whole Flex swaps.

Demo reference

The demo showcase ships a responsive screen — search the showcase for "responsive" or "breakpoints" to see live examples wired to resize events. See demo-showcase for how to run and navigate those screens.

Pitfalls

Avoid layout thrashing near thresholds. If your content is 89 cells wide and the Sm/Md threshold is 90, a single resize can flip breakpoints every frame. Use detect_transition to debounce, or widen your thresholds so the hysteresis is honest.

The breakpoint is derived from width only. Height does not feed into Breakpoint classification. If you need vertical responsiveness (short terminal vs. tall), build a separate Responsive<Flex> keyed off area.height yourself.

Where to go next