CollapsiblePanel

June 13, 2026 · View on GitHub

Click-to-expand container with a header that toggles a body of child controls.

Overview

CollapsiblePanel is a container control with a clickable header. Clicking the header (or pressing Enter/Space when focused) toggles a body that hosts any number of child controls, collapsing or expanding it vertically. When collapsed, the body's children are hidden and skipped in Tab traversal — only the header row remains visible.

It is an IControlHost container, so its body is a flat list of IWindowControl children, just like ScrollablePanelControl or ColumnContainer.

Important: CollapsiblePanel does not provide scrolling on its own. Use MaxContentHeight to cap the body height, and wrap the content in a ScrollablePanelControl if you need the capped body to scroll (see MaxContentHeight).

When to Use

CollapsiblePanel is ideal for progressive disclosure — hiding secondary content until the user asks for it:

  • FAQ / Q&A panels — each question is a header; the answer lives in the collapsible body.
  • Collapsing AI-agent secondary content — fold away reasoning steps, sub-agent calls, and tool-call transcripts so the main answer stays front and center, while still letting the user expand the detail on demand.
  • Settings sections — group related options under collapsible category headers.
  • Detail-on-demand — keep dense logs, stack traces, or raw payloads out of the way until needed.

Properties

PropertyTypeDefaultDescription
Titlestring?nullHeader title text (supports [markup] syntax)
IsExpandedbooltrueWhether the body is expanded
CollapsiblebooltrueWhen false, the panel is locked expanded with no toggle affordance and is not a Tab stop (see Panel mode)
ShowHeaderbooltrueWhen false, the header row is suppressed (see Panel mode)
HeaderStyleCollapsibleHeaderStyleBorderlessBorderless or Bordered header rendering
HeaderAlignmentHorizontalAlignmentLeftHorizontal alignment of the header content
ExpandedIconstring?Indicator shown when expanded
CollapsedIconstring?Indicator shown when collapsed
ShowHeaderSeparatorboolfalseDraw a separator under a borderless header
MaxContentHeightint?nullCaps the visible body height (in rows); clips overflow
AnimationModeCollapsibleAnimationModeNoneNone (instant) or Height (tweened body)
BorderColorColor?nullBorder color (uses theme if null)
BackgroundColorColorthemeBackground color
ForegroundColorColorthemeForeground/text color
IsEnabledbooltrueEnable/disable the panel
HasFocusboolfalseWhether the header has keyboard focus
ChildrenIReadOnlyList<IWindowControl>emptyBody child controls (read-only)

Events

EventArgumentsDescription
ExpandedChangedEventHandler<bool>Fired when expanded state changes; bool is the new IsExpanded value

Methods

MethodDescription
Expand()Expand the body
Collapse()Collapse the body
Toggle()Toggle the expanded state
AddControl(IWindowControl control)Add a child to the body
RemoveControl(IWindowControl control)Remove a child from the body
ClearControls()Remove all body children
GetChildren()Get the body child controls

Quick Start

var panel = Controls.CollapsiblePanel("Reasoning steps")
    .AddControl(Controls.Markup()
        .AddLine("[dim]1. Parsed the request[/]")
        .AddLine("[dim]2. Queried the index[/]")
        .AddLine("[dim]3. Ranked the results[/]")
        .Build())
    .Collapsed()
    .Build();

window.AddControl(panel);

Using Constructor

var panel = new CollapsiblePanel
{
    Title = "Details",
    IsExpanded = false
};

panel.AddControl(Controls.Label("Hidden until expanded"));

panel.ExpandedChanged += (sender, expanded) =>
{
    // React to expand/collapse
};

window.AddControl(panel);

Header Styles

Borderless (default)

A single clickable header row: an indicator icon followed by the title. Optionally followed by a separator line (WithHeaderSeparator()).

▸ Tool calls          (collapsed)

▾ Tool calls          (expanded)
  search("foo")
  read("bar.txt")
Controls.CollapsiblePanel("Tool calls")
    .WithHeaderStyle(CollapsibleHeaderStyle.Borderless)
    .WithHeaderSeparator()
    .AddControl(Controls.Label("search(\"foo\")"))
    .AddControl(Controls.Label("read(\"bar.txt\")"))
    .Build();

Bordered

A box (PanelControl-style) with the title embedded in the top border. The indicator icon sits next to the title in the border.

┌─ ▾ Sub-agent calls ──────────────┐
│  agent: researcher               │
│  agent: writer                   │
└──────────────────────────────────┘
Controls.CollapsiblePanel("Sub-agent calls")
    .WithHeaderStyle(CollapsibleHeaderStyle.Bordered)
    .WithBorderColor(Color.Grey37)
    .AddControl(Controls.Label("agent: researcher"))
    .AddControl(Controls.Label("agent: writer"))
    .Build();

Panel mode (non-collapsible)

Two opt-in flags let a CollapsiblePanel double as a plain panel — a titled (or titleless) container that hosts any control and never collapses.

PropertyTypeDefaultDescription
CollapsiblebooltrueWhen false, the panel is locked expanded, draws no ▾/▸ indicator, ignores header clicks and Enter/Space, and is not a Tab stop — focus passes straight through to its focusable body children. Setting it to false at runtime on a collapsed panel snaps it back to expanded.
ShowHeaderbooltrueWhen false, the header row is suppressed. Borderless → body only; Bordered → a clean titleless box framing the body.

Mutual-exclusion rule

A collapsible panel always shows its header — the header is the only toggle affordance. The effective header visibility is therefore:

effective header shown = ShowHeader || Collapsible

This means Collapsible = true, ShowHeader = false resolves gracefully to "header shown" (collapsibility wins — no exception is thrown). A headerless panel is consequently always non-collapsible.

Pass-through focus

A non-collapsible panel (Collapsible = false) is not itself a Tab stop. Tab traversal skips the panel header and lands directly on the panel's focusable body children, exactly as if they were added to a transparent container. This makes panel mode safe to drop into an existing layout without introducing an extra stop.

Combinations

CollapsibleShowHeaderHeaderStyleResult
truetrueBorderlessDefault collapsible panel
truetrueBorderedDefault bordered collapsible panel
falsetrueBorderlessStatic header (title, no indicator), body below; pass-through focus
falsetrueBorderedTitled bordered box, no collapse; pass-through focus
falsefalseBorderlessBare container, no header, no frame; pass-through focus
falsefalseBorderedClean bordered box, no title — a true "panel"
truefalse*Invalid → resolves to header shown (collapsibility wins)

Example: a bordered, headerless panel

var panel = Controls.CollapsiblePanel()
    .NonCollapsible()
    .HideHeader()
    .WithHeaderStyle(CollapsibleHeaderStyle.Bordered)
    .AddControl(Controls.Markup("[bold]Status[/]").Build())
    .AddControl(Controls.Button("Refresh")
        .OnClick((_, _, _) => { /* refresh */ })
        .Build())
    .Build();

window.AddControl(panel);

The builder methods are .Collapsible(bool), .NonCollapsible(), .ShowHeader(bool), and .HideHeader() (see the Builder Reference).

Custom Icons

Override the expand/collapse indicators with any string. Use WithIcons(expanded, collapsed) to set both at once, or WithExpandedIcon/WithCollapsedIcon individually.

// ASCII-style indicators
Controls.CollapsiblePanel("Advanced options")
    .WithIcons("[-]", "[+]")
    .AddControl(advancedOptionsPanel)
    .Build();

// Or set them separately
Controls.CollapsiblePanel("Logs")
    .WithExpandedIcon("v")
    .WithCollapsedIcon(">")
    .Build();

Header Separator

For borderless headers, draw a thin separator line under the header with WithHeaderSeparator():

Controls.CollapsiblePanel("Section")
    .WithHeaderSeparator()          // defaults to true
    .AddControl(content)
    .Build();

MaxContentHeight

WithMaxContentHeight(rows) caps the body to a fixed number of rows. Content taller than the cap is clipped. CollapsiblePanel does not scroll on its own — to get a scrollable capped body, wrap the content in a ScrollablePanelControl:

// A long transcript, capped at 8 rows and scrollable
var transcript = Controls.ScrollablePanel()
    .WithVerticalScroll()
    .Build();

for (int i = 1; i <= 100; i++)
{
    transcript.AddControl(Controls.Label($"[dim]log line {i}[/]"));
}

var panel = Controls.CollapsiblePanel("Full transcript")
    .Collapsed()
    .WithMaxContentHeight(8)        // body is at most 8 rows tall
    .AddControl(transcript)         // the inner panel scrolls within those 8 rows
    .Build();

window.AddControl(panel);

Activation

There are several ways to toggle the panel:

MechanismAction
Header clickClick the header to toggle
Enter / SpaceToggle when the header has keyboard focus
TabMove focus to the next control (body children are skipped while collapsed)
ProgrammaticSet IsExpanded, or call Toggle() / Expand() / Collapse()

ExpandedChanged Event

Subscribe to react when the state changes — for example to lazily populate the body the first time it opens:

var panel = Controls.CollapsiblePanel("Tool output")
    .Collapsed()
    .OnExpandedChanged((sender, expanded) =>
    {
        if (expanded)
        {
            windowSystem.LogService.LogInfo("Tool output panel expanded");
        }
    })
    .Build();

Or on the control directly:

panel.ExpandedChanged += (sender, expanded) =>
{
    var caption = expanded ? "Showing details" : "Details hidden";
    statusLabel?.SetContent($"[dim]{caption}[/]");
};

Programmatic Toggle

var panel = window.FindControl<CollapsiblePanel>("details");
if (panel != null)
{
    panel.Toggle();          // flip state
    // or
    panel.IsExpanded = true; // expand explicitly
}

Sizing and Alignment

Controls.CollapsiblePanel("Centered header")
    .WithWidth(50)                              // fixed width
    .WithHeaderAlignment(HorizontalAlignment.Center)
    .AddControl(content)
    .Build();
  • WithWidth(int) sets a fixed panel width (auto-sized if not set).
  • WithHeaderAlignment(HorizontalAlignment) aligns the header content (Left, Center, Right).

Animation

By default, expand/collapse re-layouts instantly. Opt into a height tween with Animated() (or WithAnimation(CollapsibleAnimationMode.Height)):

Controls.CollapsiblePanel("Animated section")
    .Animated()                                 // body height tweens open/closed
    .AddControl(content)
    .Build();

Colors

CollapsiblePanel uses PanelControl-style theme resolution plus optional overrides. It does not inherit TabControl colors — if you nest a CollapsiblePanel inside a tab, set its colors explicitly when you want them to differ from the theme.

Controls.CollapsiblePanel("Styled")
    .WithBorderColor(Color.Grey37)
    .WithBackgroundColor(new Color(30, 30, 40))
    .WithForegroundColor(Color.White)
    .Build();

Unset colors fall back to the active theme. The title accepts markup, so you can color individual parts of the header:

Controls.CollapsiblePanel("[bold cyan]Reasoning[/] [dim](3 steps)[/]")
    .Collapsed()
    .Build();

Container Behavior

CollapsiblePanel implements IControlHost, so its body can host any IWindowControl — labels, markup, lists, tables, nested panels, even another CollapsiblePanel. You can write reusable code against the capability interface:

void PopulateBody(IControlHost host)
{
    host.AddControl(Controls.Header("Settings"));
    host.AddControl(Controls.Checkbox("Enable telemetry").Build());
}

var panel = Controls.CollapsiblePanel("Preferences").Build();
PopulateBody(panel);
window.AddControl(panel);

Builder Reference

MethodDescription
Controls.CollapsiblePanel(string? title = null)Create a builder, optionally seeding the title
.WithTitle(string)Set the header title (supports markup)
.Collapsed() / .Expanded()Set the initial expanded state
.WithHeaderStyle(CollapsibleHeaderStyle)Borderless or Bordered
.WithHeaderAlignment(HorizontalAlignment)Align the header content
.WithExpandedIcon(string) / .WithCollapsedIcon(string)Set indicator icons individually
.WithIcons(expanded, collapsed)Set both indicator icons
.WithHeaderSeparator(bool = true)Draw a separator under a borderless header
.Collapsible(bool = true)Toggle collapsibility; false = panel mode (locked expanded, pass-through focus)
.NonCollapsible()Shorthand for .Collapsible(false)
.ShowHeader(bool = true)Show or suppress the header row
.HideHeader()Shorthand for .ShowHeader(false)
.WithMaxContentHeight(int)Cap the visible body height
.WithAnimation(CollapsibleAnimationMode) / .Animated()Configure expand/collapse animation
.WithWidth(int)Set fixed panel width
.WithBorderColor(Color)Set border color
.WithBackgroundColor(Color)Set background color
.WithForegroundColor(Color)Set foreground/text color
.WithName(string)Set control name for FindControl<T>
.AddControl(IWindowControl)Add a child to the body
.OnExpandedChanged(EventHandler<bool>)Subscribe to the ExpandedChanged event
.Build()Build the CollapsiblePanel

Best Practices

  1. Start collapsed for secondary content: Use .Collapsed() for reasoning, tool calls, and logs so the primary content stays visible.
  2. Cap long bodies: Use WithMaxContentHeight() to keep large content from dominating the window.
  3. Wrap to scroll: Compose with ScrollablePanelControl when a capped body needs scrolling.
  4. Use markup titles: Color or annotate header titles (e.g. step counts) with [markup] syntax.
  5. Set colors explicitly inside tabs: CollapsiblePanel does not inherit TabControl colors.

See Also


Back to Controls | Back to Main Documentation