# Resizable

Two panes and the handle that trades room between them, with minimums, collapsing and a keyboard.

```slint
import { Resizable, ResizablePane } from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 300px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            split := Resizable {
                handle-label: "Resize the panes";
                first-minimum: 120px;
                second-minimum: 160px;

                ResizablePane {
                    rect: split.first;
                    background: Tokens.color-surface-1;
                    Text {
                        text: "One";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }

                ResizablePane {
                    rect: split.second;
                    Text {
                        text: "Two";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }
            }
        }
    }
}
```

## Usage

```slint
import { Resizable, ResizablePane } from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component AppWindow inherits Window {
    width: 640px;
    height: 400px;
    background: Tokens.color-background;

    split := Resizable {
        handle-label: "Resize the tool column";
        first-minimum: 180px;
        second-minimum: 320px;

        ResizablePane {
            rect: split.first;
            // the tool column
        }

        ResizablePane {
            rect: split.second;
            // the editor
        }
    }
}
```

Slint has one `@children` slot, so the split cannot address its panes the way a web component addresses two named slots. It publishes their geometry instead — `first` and `second`, a `PaneRect` each (`x`, `y`, `width`, `height` in the split’s own coordinates) — and you wrap your own content in a `ResizablePane` bound to one of them. Nesting is the same move: drop another `Resizable` inside a pane and it fills it, with its own ratio, its own minimums and its own handle.

**The state is one number.** `ratio` is the first pane’s share of the resizable axis, and every path into it — dragging the handle, the arrow keys, `Home` / `End`, the accessible increment, decrement and set-value actions — goes through `set-ratio`, which bounds the move against both minimums first. A pane can never be squeezed below the size its content needs, and a move that changes nothing stays silent: a key held at a bound does not spray `changed`. A `ratio` written from outside is corrected in place rather than left contradicting the panes it claims to describe, so what you read back is the split you are looking at.

## Examples

### A vertical split

`orientation` decides which way the panes sit: `ResizableOrientation.horizontal` puts them side by side with an upright handle between them, and `ResizableOrientation.vertical` stacks them. The handle answers the arrows that run along its own axis and leaves the other pair to whatever else is on screen.

```slint
import {
    Resizable, ResizableOrientation, ResizablePane,
} from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 480px;
    height: 340px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            split := Resizable {
                orientation: ResizableOrientation.vertical;
                handle-label: "Resize the preview";
                first-minimum: 80px;
                second-minimum: 80px;
                ratio: 0.6;

                ResizablePane {
                    rect: split.first;
                    background: Tokens.color-surface-1;
                    Text {
                        text: "Preview";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }

                ResizablePane {
                    rect: split.second;
                    Text {
                        text: "Console";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }
            }
        }
    }
}
```

### Minimums

`first-minimum` and `second-minimum` are the smallest each pane may become. A split too small to honour both keeps the first pane’s and lets the second go under — one of them has to give, and the first is the one the ratio measures.

```slint
import { Resizable, ResizablePane } from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 300px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        spacing: 12px;

        Text {
            text: "Drag the handle: neither side goes under 200 pixels.";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            split := Resizable {
                handle-label: "Resize the panes";
                first-minimum: 200px;
                second-minimum: 200px;

                ResizablePane {
                    rect: split.first;
                    background: Tokens.color-surface-1;
                    Text {
                        text: "At least 200px";
                        color: Tokens.color-muted-foreground;
                        font-size: Tokens.typography-body-sm-size;
                    }
                }

                ResizablePane {
                    rect: split.second;
                    Text {
                        text: "At least 200px";
                        color: Tokens.color-muted-foreground;
                        font-size: Tokens.typography-body-sm-size;
                    }
                }
            }
        }
    }
}
```

### A pane that shuts

A `first-collapsible` or `second-collapsible` pane has one more rest position below its minimum: shut, at `collapsed-length`. It still holds at the minimum until the move asks for less than halfway to that, and reopens at the minimum on the way back — the sidebar-collapse gesture, with the same hysteresis in both directions. `first-collapsed` and `second-collapsed` say which state it is in, so a rail can bind its contents to them.

```slint
import { Icon } from "@glint/components/icon.slint";
import { Resizable, ResizablePane } from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;
        spacing: 12px;

        Text {
            text: "Drag the handle left, or press Home with it focused.";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            split := Resizable {
                handle-label: "Resize the tool column";
                handle-grip: true;
                first-collapsible: true;
                first-minimum: 180px;
                collapsed-length: 52px;
                ratio: 0.35;

                ResizablePane {
                    rect: split.first;
                    background: Tokens.color-surface-1;

                    // The rail the column shuts to, and the column itself.
                    if split.first-collapsed: VerticalLayout {
                        alignment: start;
                        padding: 16px;
                        Icon {
                            icon: IconSet.PanelLeft;
                            size: 18px;
                            tint: Tokens.color-muted-foreground;
                        }
                    }
                    if !split.first-collapsed: VerticalLayout {
                        alignment: start;
                        padding: 16px;
                        spacing: 8px;
                        Text {
                            text: "Files";
                            color: Tokens.color-foreground;
                            font-weight: Tokens.typography-weight-semibold;
                        }
                        Text {
                            text: "main.slint";
                            color: Tokens.color-muted-foreground;
                            font-size: Tokens.typography-body-sm-size;
                        }
                    }
                }

                ResizablePane {
                    rect: split.second;
                    Text {
                        text: "Editor";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }
            }
        }
    }
}
```

### Nested splits

A `Resizable` dropped inside a pane fills it and keeps its own ratio, minimums and handle. `handle-thickness` is the room reserved between the panes and also the handle’s hit area.

```slint
import {
    Resizable, ResizableOrientation, ResizablePane,
} from "@glint/components/resizable.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 600px;
    height: 340px;
    background: Tokens.color-background;

    VerticalLayout {
        padding: 24px;

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            outer := Resizable {
                handle-label: "Resize the sidebar";
                first-minimum: 120px;
                second-minimum: 200px;
                ratio: 0.3;

                ResizablePane {
                    rect: outer.first;
                    background: Tokens.color-surface-1;
                    Text {
                        text: "Sidebar";
                        color: Tokens.color-foreground;
                        font-weight: Tokens.typography-weight-semibold;
                    }
                }

                ResizablePane {
                    rect: outer.second;

                    inner := Resizable {
                        orientation: ResizableOrientation.vertical;
                        handle-label: "Resize the console";
                        first-minimum: 80px;
                        second-minimum: 60px;
                        ratio: 0.65;

                        ResizablePane {
                            rect: inner.first;
                            Text {
                                text: "Editor";
                                color: Tokens.color-foreground;
                                font-weight: Tokens.typography-weight-semibold;
                            }
                        }

                        ResizablePane {
                            rect: inner.second;
                            background: Tokens.color-surface-1;
                            Text {
                                text: "Console";
                                color: Tokens.color-foreground;
                                font-weight: Tokens.typography-weight-semibold;
                            }
                        }
                    }
                }
            }
        }
    }
}
```

### Disabled

`disabled: true` dims the handle and stops it responding — to the pointer, to the keyboard and to the accessible actions alike, since all three go through the same door.

```slint
import { Resizable, ResizablePane } from "@glint/components/resizable.slint";
import { Switch } from "@glint/components/switch.slint";
import { Tokens } from "@glint/theme/tokens.slint";

export component Demo inherits Window {
    width: 560px;
    height: 320px;
    background: Tokens.color-background;

    in-out property <bool> locked: true;

    VerticalLayout {
        padding: 24px;
        spacing: 12px;

        Switch {
            label: "Lock the layout";
            checked <=> root.locked;
        }

        Rectangle {
            border-width: 1px;
            border-color: Tokens.color-border-hairline;
            border-radius: Tokens.radius-md;
            clip: true;

            split := Resizable {
                handle-label: "Resize the panes";
                handle-grip: true;
                disabled: root.locked;
                first-minimum: 120px;
                second-minimum: 120px;

                ResizablePane {
                    rect: split.first;
                    background: Tokens.color-surface-1;
                    Text {
                        text: "One";
                        color: Tokens.color-foreground;
                    }
                }

                ResizablePane {
                    rect: split.second;
                    Text {
                        text: "Two";
                        color: Tokens.color-foreground;
                    }
                }
            }
        }
    }
}
```

## API Reference

### Properties

| Property             | Type                      | Default                           | Description                                                                                                                                                                                                                                                                                                                        |
| -------------------- | ------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `orientation`        | `in ResizableOrientation` | `ResizableOrientation.horizontal` | Which way the panes sit; also the axis the handle moves along.                                                                                                                                                                                                                                                                     |
| `ratio`              | `in-out float`            | `0.5`                             | Two-way; the first pane's share of the resizable axis, 0..1. Writes from outside are bounded by the minimums, same as a drag.                                                                                                                                                                                                      |
| `first-minimum`      | `in length`               | `0px`                             | Smallest the first pane may become.                                                                                                                                                                                                                                                                                                |
| `second-minimum`     | `in length`               | `0px`                             | Smallest the second pane may become.                                                                                                                                                                                                                                                                                               |
| `first-collapsible`  | `in bool`                 | `false`                           | Whether each pane may be shut altogether. A collapsible pane still holds at its minimum, but a move that asks for less than halfway between that minimum and `collapsed-length` snaps it shut instead, and the way back out reopens it at the minimum — the sidebar-collapse gesture, with the same hysteresis in both directions. |
| `second-collapsible` | `in bool`                 | `false`                           |                                                                                                                                                                                                                                                                                                                                    |
| `collapsed-length`   | `in length`               | `0px`                             | What a shut pane measures along the resizable axis: a rail's width, or `0px` for a pane that disappears. One value for both, because a split that collapses on both ends collapses to the same rail.                                                                                                                               |
| `first-collapsed`    | `out bool`                | no default                        | True while that pane is shut. Bind a rail's contents to it, or persist it beside `ratio`.                                                                                                                                                                                                                                          |
| `second-collapsed`   | `out bool`                | no default                        |                                                                                                                                                                                                                                                                                                                                    |
| `handle-thickness`   | `in length`               | `8px`                             | Room reserved for the handle between the panes; also its hit area.                                                                                                                                                                                                                                                                 |
| `handle-grip`        | `in bool`                 | `false`                           | Draw a grip on the handle, so the split can be found before the pointer is on it.                                                                                                                                                                                                                                                  |
| `keyboard-step`      | `in length`               | `16px`                            | How far one arrow key — or one accessible increment / decrement — moves the split.                                                                                                                                                                                                                                                 |
| `disabled`           | `in bool`                 | `false`                           | When true, the handle dims and stops responding.                                                                                                                                                                                                                                                                                   |
| `handle-label`       | `in string`               | `@tr("Resize panes")`             | Accessible name of the handle; override to name and translate it.                                                                                                                                                                                                                                                                  |
| `first`              | `out PaneRect`            | no default                        | The panes' boxes — hand each to a `ResizablePane`.                                                                                                                                                                                                                                                                                 |
| `second`             | `out PaneRect`            | no default                        |                                                                                                                                                                                                                                                                                                                                    |
| `first-length`       | `out length`              | no default                        | Size of each pane along the resizable axis — the bounded truth, as opposed to `ratio`, which is only the intent. Everything the split says about itself, to a consumer's layout and to assistive technology alike, is derived from here, so a ratio that has not been reconciled yet can never be drawn — or announced.            |
| `second-length`      | `out length`              | no default                        |                                                                                                                                                                                                                                                                                                                                    |

### Callbacks

| Callback         | Description                                                   |
| ---------------- | ------------------------------------------------------------- |
| `changed(float)` | Fired with the new `ratio` whenever the user moves the split. |

### Functions

| Function              | Description                                                            |
| --------------------- | ---------------------------------------------------------------------- |
| `set-ratio(r: float)` | The one door every user interaction goes through: bound, then publish. |
| `increment()`         | Gives the first pane one `keyboard-step` more, bounded.                |
| `decrement()`         | Takes one `keyboard-step` from it, bounded.                            |

### Enums

| Enum                   | Values                   |
| ---------------------- | ------------------------ |
| `ResizableOrientation` | `horizontal`, `vertical` |

### ResizablePane

One side of the split: it takes a `PaneRect` from the `Resizable` above it and carries your content in its `@children` slot, clipped to that box.

### Properties

| Property | Type          | Default    | Description                                               |
| -------- | ------------- | ---------- | --------------------------------------------------------- |
| `rect`   | `in PaneRect` | no default | The geometry to occupy — `split.first` or `split.second`. |

`PaneRect` is a data type rather than a component — `x`, `y`, `width` and `height`, in the split’s own coordinates. Feed it to a `ResizablePane`; nothing else reads it.

## Accessibility

- **The handle is the control, and it speaks as a slider.** Slint has no separator role (ADR-0015), and a slider is the role that carries a value, the range it lives in, the axis it runs along and the actions that move it. Name it with `handle-label`.
- **The value is the first pane’s percentage of the axis**, and it is read off the pane rather than off `ratio` — so the value can never fall outside the bounds announced beside it. `accessible-value-minimum` and `accessible-value-maximum` are the first pane’s reachable bounds expressed the same way — including a collapsed bound when either pane is collapsible — and `accessible-value-step` is `keyboard-step` as a percentage.
- **Accessible actions.** Increment and decrement move the split one `keyboard-step`; set-value takes a percentage. All three go through the same bounding door the pointer and the keyboard do, and all three are refused while `disabled`.
- **Keyboard.** `Tab` focuses the handle — `resizable.focus()` lands there too, it being the split’s only focusable part. `Left` / `Right` step a horizontal split and `Up` / `Down` a vertical one, by `keyboard-step`; `Home` and `End` run it to either end, which for a collapsible pane is shut rather than its minimum. A step has no threshold to cross the way a drag does, so a collapsible pane sitting on its minimum shuts on the next step in and reopens at that minimum on the step back out.
- **Keyboard-only focus ring.** The ring is drawn around the handle when the split is reached from the keyboard, and not when it is pressed with a pointer.
- **`handle-grip` is findability, not affordance.** The chip astride the hairline is wider than the hit strip on purpose — it makes the split findable before the pointer is on it — and it draws only; the handle still owns every pointer event over it.
