# How to Implement Multi-Pane Navigation with ChildPanels in Decompose

> Learn to implement multi-pane navigation with ChildPanels in Decompose. Create responsive layouts with Main, Details, and Extra panels adapting automatically to SINGLE, DUAL, and TRIPLE modes.

- Repository: [Arkadii Ivanov/decompose](https://github.com/arkivanov/decompose)
- Tags: how-to-guide
- Published: 2026-02-25

---

**The ChildPanels navigation model in Decompose enables responsive multi-pane layouts by composing up to three panels—Main, Details, and Extra—with automatic UI adaptation across SINGLE, DUAL, and TRIPLE modes.**

The Decompose library by Arkadii Ivanov provides a robust navigation solution for Kotlin Multiplatform projects, with the ChildPanels API specifically designed for building responsive master-detail interfaces. This guide demonstrates how to implement multi-pane navigation with ChildPanels using the actual source implementation from the `arkivanov/decompose` repository, covering state management, navigation patterns, and Jetpack Compose integration.

## Core Architecture of ChildPanels

The Child Panels model consists of three integrated layers that manage the lifecycle and presentation of up to three simultaneous panels.

### The ChildPanels State Holder

At the center of the system is the `ChildPanels` data class defined in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanels.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/ChildPanels.kt) (lines 21–27). This immutable state holder contains the three possible child components—**Main**, **Details**, and **Extra**—along with the current `ChildPanelsMode` (SINGLE, DUAL, or TRIPLE). The class implements `Serializable` for automatic state preservation across process death.

### The PanelsNavigator Interface

Navigation mutations occur through `PanelsNavigator`, which exposes imperative methods like `activateDetails()`, `dismissDetails()`, and `setMode()`. These extension functions are implemented in [`decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsNavigatorExt.kt`](https://github.com/arkivanov/decompose/blob/main/decompose/src/commonMain/kotlin/com/arkivanov/decompose/router/panels/PanelsNavigatorExt.kt) (lines 85–94) and operate on the `PanelsNavigation` source to update the `ChildPanels` state atomically.

### The ChildPanels Composable

The experimental Compose extension `ChildPanels` (located in [`extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/panels/ChildPanels.kt`](https://github.com/arkivanov/decompose/blob/main/extensions-compose-experimental/src/commonMain/kotlin/com/arkivanov/decompose/extensions/compose/experimental/panels/ChildPanels.kt), lines 48–71) observes a `Value<ChildPanels>` and delegates layout decisions to a `ChildPanelsLayout` implementation. The default `HorizontalChildPanelsLayout` adapts its measuring policy based on the current mode, using configurable weights for DUAL and TRIPLE configurations.

## Setting Up the Navigation Source

First, instantiate a `PanelsNavigation` source with appropriate configuration serializers, then initialize the navigation model using the `childPanels` extension on your `ComponentContext`.

```kotlin
// In your root component class
private val navigation = PanelsNavigation<Unit, DetailsConfig, Nothing>()

val panels: Value<ChildPanels<*, MainComponent, *, DetailsComponent, Nothing, Nothing>> =
    childPanels(
        source = navigation,
        serializers = Unit.serializer() to DetailsConfig.serializer(),
        initialPanels = { Panels(main = Unit) },
        handleBackButton = true,
        mainFactory = { _, ctx -> MainComponentImpl(ctx) },
        detailsFactory = { cfg, ctx ->
            DetailsComponentImpl(
                ctx,
                itemId = cfg.itemId,
                onFinished = navigation::dismissDetails
            )
        }
    )

```

This setup creates a navigation graph where the Main panel is always present, while Details and Extra panels can be activated or dismissed dynamically. The `serializers` parameter enables automatic state saving; pass `null` to both positions if you wish to disable state preservation.

## Building the Multi-Pane UI

The UI layer consumes the `Value<ChildPanels>` and renders panels according to the current mode and available screen real estate.

### Configuring the Layout Mode

The `ChildPanelsMode` enum determines how many panels display simultaneously:
- **SINGLE**: Shows only the active panel (typical for phones)
- **DUAL**: Splits space between Main and Details (tablets)
- **TRIPLE**: Displays Main, Details, and Extra side-by-side (desktop)

Mode switching is a pure UI concern. Use `PanelsNavigator.setMode()` to update the state, triggering recomposition without affecting the back stack.

### Implementing HorizontalChildPanelsLayout

The `HorizontalChildPanelsLayout` class (defined in [`extensions-compose-experimental/.../HorizontalChildPanelsLayout.kt`](https://github.com/arkivanov/decompose/blob/main/extensions-compose-experimental/.../HorizontalChildPanelsLayout.kt)) implements the measuring logic for responsive panel sizing. It accepts weight parameters for DUAL and TRIPLE configurations, converting them to concrete pixel widths via `DualMeasurePolicy` (lines 98–112) and `TripleMeasurePolicy`.

```kotlin
@Composable
fun MultiPaneContent(component: MultiPaneComponent) {
    val panels by component.panels.subscribeAsState()

    BoxWithConstraints(Modifier.fillMaxSize()) {
        // Main ChildPanels composable
        ChildPanels(
            panels = panels,
            mainChild = { MainContent(it.instance) },
            detailsChild = { DetailsContent(it.instance) },
            extraChild = { ExtraContent(it.instance) },
            layout = HorizontalChildPanelsLayout(
                dualWeights = Pair(0.3F, 0.7F),
                tripleWeights = Triple(0.3F, 0.4F, 0.3F)
            ),
            secondPanelPlaceholder = { Text("Select an article") },
            animators = ChildPanelsAnimators(
                single = fade() + scale(),
                dual = fade() to fade()
            ),
            predictiveBackParams = {
                PredictiveBackParams(
                    backHandler = component.backHandler,
                    onBack = component::onBack,
                    animatable = ::materialPredictiveBackAnimatable
                )
            }
        )

        // Responsive mode switching based on viewport width
        val mode = when {
            maxWidth >= 1200.dp -> ChildPanelsMode.TRIPLE
            maxWidth >= 800.dp -> ChildPanelsMode.DUAL
            else -> ChildPanelsMode.SINGLE
        }
        
        DisposableEffect(mode) { 
            component.setMode(mode) 
            onDispose {} 
        }
    }
}

```

The `ChildPanels` composable automatically handles placeholder visibility when panels are inactive and applies the specified `ChildPanelsAnimators` during mode transitions or navigation changes.

## Navigating Between Panels

Use the navigator extension functions to manipulate the panel stack. These methods update the `ChildPanels` state, triggering UI recomposition.

```kotlin
// Activate Details from Main panel
onItemSelected = { itemId ->
    navigation.activateDetails(details = DetailsConfig(itemId = itemId))
}

// Activate Extra panel from Details (e.g., showing author info)
onShowAuthor = { authorId ->
    navigation.activateExtra(extra = AuthorConfig(authorId = authorId))
}

// Dismiss panels (typically called on back press)
navigation.dismissDetails()  // Returns to Main-only view
navigation.dismissExtra()    // Returns to Main+Details view
navigation.pop()             // Pops the top-most panel regardless of type

```

These navigation actions are defined in [`PanelsNavigatorExt.kt`](https://github.com/arkivanov/decompose/blob/main/PanelsNavigatorExt.kt) and maintain proper back stack ordering. When `handleBackButton` is enabled during initialization, the navigation automatically intercepts back presses to dismiss panels in reverse activation order.

## Customizing Layout Weights

For granular control over panel proportions, customize the weights passed to `HorizontalChildPanelsLayout`. These values determine the fractional width allocated to each panel in DUAL and TRIPLE modes.

```kotlin
val customLayout = HorizontalChildPanelsLayout(
    dualWeights = Pair(0.25F, 0.75F),          // Main takes 25%, Details 75%
    tripleWeights = Triple(0.2F, 0.5F, 0.3F)  // Main/Details/Extra split
)

```

The layout engine applies these weights during the measure phase, ensuring panels resize smoothly when switching between modes or rotating devices.

## Summary

- **ChildPanels** manages up to three panels (Main, Details, Extra) with lifecycle awareness and state preservation via `PanelsNavigation` and the `childPanels()` factory.
- **ChildPanelsMode** (SINGLE/DUAL/TRIPLE) controls the visual layout independently from the navigation state, enabling responsive designs that adapt to screen width.
- **HorizontalChildPanelsLayout** implements the standard responsive behavior with configurable weight parameters for proportional sizing in multi-pane configurations.
- Navigation mutations use imperative extension functions (`activateDetails`, `dismissDetails`, `setMode`) that update the observable `ChildPanels` state, driving automatic UI recomposition.
- The experimental Compose extension provides `ChildPanels` composable with built-in support for placeholders, predictive back gestures, and custom animators.

## Frequently Asked Questions

### What is the maximum number of panels supported by ChildPanels?

ChildPanels supports exactly three panels: **Main**, **Details**, and **Extra**. The Main panel is typically the master view (e.g., a list), while Details and Extra represent increasingly specific content layers. The API is designed around this three-panel constraint, with `HorizontalChildPanelsLayout` providing optimized measuring policies for each combination of active panels.

### How does ChildPanels handle state preservation during configuration changes?

State preservation relies on Kotlin serialization. When creating `PanelsNavigation`, you provide `serializers` for your configuration classes (MainConfig, DetailsConfig, etc.). The `ChildPanels` data class automatically serializes the current panel configurations and active mode during `onSaveInstanceState`, restoring them when the process recreates. Pass `null` to disable this behavior if your configurations contain non-serializable data.

### Can I implement custom layouts beyond the horizontal arrangement?

Yes. While `HorizontalChildPanelsLayout` provides the standard left-to-right arrangement with weight-based sizing, you can implement the `ChildPanelsLayout` interface (defined in [`extensions-compose-experimental/.../ChildPanelsLayout.kt`](https://github.com/arkivanov/decompose/blob/main/extensions-compose-experimental/.../ChildPanelsLayout.kt), lines 12–27) to create vertical stacks, overlapping panels, or context-aware arrangements. The `ChildPanels` composable accepts any `ChildPanelsLayout` implementation via its `layout` parameter.

### How do I prevent the Details panel from appearing in SINGLE mode on phones?

The panel visibility logic is mode-driven, not device-driven. When `ChildPanelsMode` is set to SINGLE, only the active panel renders. To ensure phones never show split panes, detect the screen width (as shown in the `BoxWithConstraints` example) and set the mode to SINGLE when `maxWidth < 800.dp`. The placeholder content for inactive panels only appears in DUAL or TRIPLE modes when the corresponding panel slot is empty.