ResponsiveWidget
Create adaptive UI layouts that change based on terminal size.
ResponsiveWidget displays the first child whose condition evaluates to true, allowing you to create terminal applications that adapt their layout based on available space. This is similar to CSS media queries or responsive web design, but for terminal user interfaces.
Basic Usage
Create a responsive layout using the fluent API with condition builders. The example below shows a complete application layout with navigation, primary content, and secondary content panels that reorganize based on terminal width:
dotnet runThis demo showcases:
- Theme panels with color-coded borders indicating panel priority (cyan for navigation, green for primary, yellow for secondary)
- Splitter layout separating navigation from content in wider views
- HStack/VStack switching - content panels appear side-by-side in extra wide terminals (≥120 cols) and stack vertically in wide terminals (≥100 cols)
- Progressive degradation - secondary panel disappears in medium terminals (≥80 cols), and splitter becomes vertical stack in narrow terminals (<80 cols)
Dynamic Evaluation
Conditions are evaluated during the layout phase using the actual available space from parent constraints. This happens automatically whenever the terminal is resized or the layout changes. Try resizing your browser window in the demo above to see the layout adapt!
Simple Breakpoint
The simplest responsive pattern switches between two layouts based on terminal width:
When the terminal is wide enough (≥80 columns), the first condition matches:
When the terminal is narrower (<80 columns), the Otherwise fallback is used.
Responsive Layouts
A common pattern is switching between horizontal and vertical layouts based on available width:
In wide terminals (≥100 columns), content appears side-by-side:
In narrow terminals, the same content stacks vertically to fit the available space.
Multiple Breakpoints
Define multiple conditions to create progressively enhanced layouts:
Extra wide terminals (≥120 columns) show three columns:
Wide terminals (≥80 columns) show two columns:
Medium terminals (≥40 columns) show a single column with details:
Narrow terminals (<40 columns) show minimal compact view.
Evaluation Order
Conditions are evaluated in order from top to bottom. The first matching condition wins, so place more specific conditions before less specific ones.
Condition Builders
ResponsiveWidget provides several helper methods for common patterns:
WhenMinWidth
Test for a minimum terminal width:
ctx.Responsive(r => [
r.WhenMinWidth(100, r => r.Text("Wide")),
r.Otherwise(r => r.Text("Narrow"))
])2
3
4
WhenWidth
Custom width-only condition:
ctx.Responsive(r => [
r.WhenWidth(w => w >= 80 && w < 120, r => r.Text("Medium")),
r.WhenWidth(w => w >= 120, r => r.Text("Wide")),
r.Otherwise(r => r.Text("Narrow"))
])2
3
4
5
When
Full control with width and height:
The When method receives both availableWidth and availableHeight parameters.
Otherwise
Fallback for when no other condition matches:
ctx.Responsive(r => [
r.WhenMinWidth(100, r => r.Text("Specific layout")),
r.WhenMinWidth(50, r => r.Text("Another layout")),
r.Otherwise(r => r.Text("Default fallback"))
])2
3
4
5
Always Include Otherwise
Always include an Otherwise branch as your last condition to ensure something is displayed even if no other condition matches.
How It Works
ResponsiveWidget works by:
- Reconciliation: All child branches are reconciled into nodes upfront
- Evaluation: During
Measure(), conditions are evaluated against available space - Activation: Only the first matching branch's node is measured, arranged, and rendered
- Re-evaluation: If terminal size changes, conditions are re-evaluated automatically
Terminal Resize → Layout Pass → Measure() → Evaluate Conditions → Render Active BranchState Preservation
Each branch maintains its own focus state and internal state:
var listState = new ListState();
ctx.Responsive(r => [
r.WhenMinWidth(100, r =>
r.List(items).WithState(listState) // Wide layout
),
r.Otherwise(r =>
r.List(items).WithState(listState) // Narrow layout - same state
)
])2
3
4
5
6
7
8
9
10
When switching between branches due to resize, the shared state (like listState) is preserved.
State Management
For widgets with internal state (like List or TextBox), use the state management pattern to share state across responsive branches. This prevents losing focus or scroll position when the layout switches.
Layout Behavior
ResponsiveWidget has minimal overhead:
- Measuring: The active branch determines the size
- Arranging: The active branch fills the entire bounds
- Rendering: Only the active branch renders
- Focus: Only the active branch's focusable nodes are available
Inactive branches are reconciled but not measured, arranged, or rendered.
Common Patterns
Dashboard Layouts
ctx.Responsive(r => [
// Desktop: Multi-column dashboard
r.WhenMinWidth(120, r =>
r.HStack(h => [
h.Border(b => statsWidget, title: "Stats").FillWidth(1),
h.Border(b => mainWidget, title: "Main").FillWidth(2),
h.Border(b => activityWidget, title: "Activity").FillWidth(1)
])
),
// Tablet: Two-column
r.WhenMinWidth(80, r =>
r.HStack(h => [
h.VStack(v => [
v.Border(b => statsWidget, title: "Stats"),
v.Border(b => activityWidget, title: "Activity")
]).FillWidth(1),
h.Border(b => mainWidget, title: "Main").FillWidth(2)
])
),
// Mobile: Single column
r.Otherwise(r =>
r.VStack(v => [
v.Border(b => statsWidget, title: "Stats"),
v.Border(b => mainWidget, title: "Main"),
v.Border(b => activityWidget, title: "Activity")
])
)
])2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Form Layouts
ctx.Responsive(r => [
// Wide: Labels and inputs side-by-side
r.WhenMinWidth(80, r =>
r.VStack(v => [
v.HStack(h => [
h.Text("Name:").FixedWidth(20),
h.TextBox(state.Name).FillWidth()
]),
v.HStack(h => [
h.Text("Email:").FixedWidth(20),
h.TextBox(state.Email).FillWidth()
])
])
),
// Narrow: Labels above inputs
r.Otherwise(r =>
r.VStack(v => [
v.Text("Name:"),
v.TextBox(state.Name),
v.Text("Email:"),
v.TextBox(state.Email)
])
)
])2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Detail/Master Views
ctx.Responsive(r => [
// Wide: Side-by-side
r.WhenMinWidth(100, r =>
r.HStack(h => [
h.List(items).FillWidth(1),
h.Border(b => detailView, title: "Details").FillWidth(2)
])
),
// Narrow: List only (navigate to see details)
r.Otherwise(r =>
r.List(items)
)
])2
3
4
5
6
7
8
9
10
11
12
13
14
Adaptive Controls
Show different levels of detail in controls:
ctx.Responsive(r => [
// Wide: Full control with labels
r.WhenMinWidth(80, r =>
r.HStack(h => [
h.Button("Save (Ctrl+S)").OnClick(save),
h.Button("Cancel (Esc)").OnClick(cancel),
h.Button("Help (F1)").OnClick(help)
])
),
// Narrow: Icon/short text only
r.Otherwise(r =>
r.HStack(h => [
h.Button("[S]").OnClick(save),
h.Button("[C]").OnClick(cancel),
h.Button("[?]").OnClick(help)
])
)
])2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Testing Responsive Layouts
Test your responsive layouts at different terminal sizes:
[Theory]
[InlineData(120, 30)] // Wide
[InlineData(80, 30)] // Medium
[InlineData(40, 30)] // Narrow
public async Task ResponsiveLayout_AdaptsToTerminalSize(int width, int height)
{
await using var terminal = Hex1bTerminal.CreateBuilder()
.WithTestTerminal(width, height)
.WithHex1bApp((app, options) => ctx =>
ctx.Responsive(r => [
r.WhenMinWidth(100, r => r.Text("Wide")),
r.WhenMinWidth(60, r => r.Text("Medium")),
r.Otherwise(r => r.Text("Narrow"))
])
)
.Build();
await terminal.RunAsync();
// Verify the correct layout is active
// ...
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Performance Considerations
- Reconciliation: All branches are reconciled once at startup
- Switching: Changing branches during resize is very fast (no re-reconciliation)
- Memory: All branches remain in memory (inactive nodes are not disposed)
For most applications, this overhead is negligible. If you have many complex branches or very deep widget trees, consider:
- Lazy loading expensive widgets only when their branch activates
- Sharing common widgets across branches
- Using fewer breakpoints
Related Widgets
- HStackWidget - Horizontal layouts often used in wide breakpoints
- VStackWidget - Vertical layouts often used in narrow breakpoints
- BorderWidget - Often combined with responsive layouts for visual structure
- SplitterWidget - Divides space between panels, useful in responsive layouts