# InputGroup

A text input with things welded to its edges — icons, text, a spinner, a control of your own.

```slint
import { InputGroup } from "@glint/components/input-group.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

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

    in-out property <string> search-query: "";

    VerticalLayout {
        alignment: center;
        spacing: 16px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            VerticalLayout {
                width: 360px;

                InputGroup {
                    leading-icon: IconSet.Search;
                    placeholder: "Search documentation…";
                    trailing-text: "⌘K";
                    text <=> root.search-query;
                }
            }
        }

        Text {
            text: root.search-query != "" ? "Searching: " + root.search-query : "Ready to search";
            color: Tokens.color-muted-foreground;
            font-size: Tokens.typography-body-sm-size;
            horizontal-alignment: center;
        }
    }
}
```

## Usage

```slint
import { InputGroup } from "@glint/components/input-group.slint";
import { IconSet } from "@lucide";

export component AppWindow inherits Window {
    in-out property <string> url: "";

    VerticalLayout {
        alignment: center;

        InputGroup {
            leading-text: "https://";
            trailing-icon: IconSet.ExternalLink;
            placeholder: "example.com";
            text <=> root.url;
        }
    }
}
```

`InputGroup` embeds prefix and suffix adornments (icons, static text labels, spinners, or slotted custom chips) directly inside the shared border and focus ring of a text entry.

## Examples

### Input group variants

The `variant` property switches between single-line text, password entry, and a growing multi-line textarea:

- `InputGroupVariant.text`: Standard single-line entry.
- `InputGroupVariant.password`: Obscured password characters.
- `InputGroupVariant.multi-line`: Dynamic multi-line composer that grows with content.

```slint
import { InputGroup, InputGroupVariant } from "@glint/components/input-group.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

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

    VerticalLayout {
        alignment: center;
        spacing: 16px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            VerticalLayout {
                width: 380px;
                spacing: 12px;

                InputGroup {
                    variant: InputGroupVariant.text;
                    leading-icon: IconSet.Mail;
                    placeholder: "user@domain.com";
                }

                InputGroup {
                    variant: InputGroupVariant.password;
                    leading-icon: IconSet.Lock;
                    placeholder: "Master key";
                }

                InputGroup {
                    variant: InputGroupVariant.multi-line;
                    leading-icon: IconSet.MessageSquare;
                    placeholder: "Write a comment or prompt…";
                }
            }
        }
    }
}
```

### Addon alignments and slotted children

The `@children` slot accepts custom elements placed according to `addon-align`:

- `InputGroupAddonAlign.inline-start`: Beside the text at the leading edge.
- `InputGroupAddonAlign.inline-end`: Beside the text at the trailing edge.
- `InputGroupAddonAlign.block-start`: Header band above the text area.
- `InputGroupAddonAlign.block-end`: Footer toolbar band below the text area.

```slint
import { InputGroup, InputGroupVariant, InputGroupAddonAlign } from "@glint/components/input-group.slint";
import { Kbd } from "@glint/components/kbd.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

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

    VerticalLayout {
        alignment: center;
        spacing: 14px;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            VerticalLayout {
                width: 380px;
                spacing: 12px;

                InputGroup {
                    addon-align: InputGroupAddonAlign.inline-end;
                    placeholder: "Jump to file…";
                    leading-icon: IconSet.FileText;

                    Kbd {
                        text: "Ctrl+P";
                    }
                }

                InputGroup {
                    addon-align: InputGroupAddonAlign.inline-start;
                    placeholder: "Custom leading badge";
                    leading-text: "v2.0";
                }

                InputGroup {
                    variant: InputGroupVariant.multi-line;
                    addon-align: InputGroupAddonAlign.block-start;
                    placeholder: "Header row above text";

                    Text {
                        text: "Markdown Supported";
                        color: Tokens.color-muted-foreground;
                        font-size: Tokens.typography-body-sm-size;
                    }
                }

                InputGroup {
                    variant: InputGroupVariant.multi-line;
                    addon-align: InputGroupAddonAlign.block-end;
                    placeholder: "Footer toolbar below text";

                    Text {
                        text: "Press Shift+Enter for newline";
                        color: Tokens.color-muted-foreground;
                        font-size: Tokens.typography-body-sm-size;
                    }
                }
            }
        }
    }
}
```

### Asynchronous loading state

Set `loading: true` to display an animated spinner in the trailing slot, indicating background validation or asynchronous lookups.

```slint
import { InputGroup } from "@glint/components/input-group.slint";
import { Tokens } from "@glint/theme/tokens.slint";
import { IconSet } from "@lucide";

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

    VerticalLayout {
        alignment: center;
        padding: 24px;

        HorizontalLayout {
            alignment: center;

            VerticalLayout {
                width: 360px;

                InputGroup {
                    leading-icon: IconSet.User;
                    text: "felipemachado";
                    loading: true;
                    loading-label: "Checking username availability…";
                }
            }
        }
    }
}
```

## API Reference

### Properties

| Property        | Type                      | Default                           | Description                                                                                                                                                                                                                         |
| --------------- | ------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `text`          | `in-out string`           | no default                        | Two-way bound to the field contents.                                                                                                                                                                                                |
| `placeholder`   | `in string`               | no default                        | Hint shown when the field is empty.                                                                                                                                                                                                 |
| `enabled`       | `in bool`                 | `true`                            | When false, the group dims and stops accepting keystrokes.                                                                                                                                                                          |
| `read-only`     | `in bool`                 | `false`                           | When true, the user can focus but not edit.                                                                                                                                                                                         |
| `variant`       | `in InputGroupVariant`    | `InputGroupVariant.text`          | What the entry holds — see `InputGroupVariant`. A `multi-line` group grows with its text, up to the `max-height` the call site sets; past that the text scrolls and the caret is scrolled back into view, exactly as in `Textarea`. |
| `invalid`       | `in bool`                 | `false`                           | When true, paints the border in destructive state.                                                                                                                                                                                  |
| `loading`       | `in bool`                 | `false`                           | While true, a spinner turns in the trailing addon — the async check (a username lookup, an address validation) reported inside the field rather than beside it.                                                                     |
| `loading-label` | `in string`               | `@tr("Loading")`                  | Name of that spinner. Overridable because the group owns the element and a consumer cannot reach it (same reason Button exposes it).                                                                                                |
| `leading-icon`  | `in LucideIcon`           | no default                        | Addon in front of the text — normally an `IconSet` icon, a prefix, or both.                                                                                                                                                         |
| `leading-text`  | `in string`               | no default                        |                                                                                                                                                                                                                                     |
| `trailing-icon` | `in LucideIcon`           | no default                        | Addon behind the text — a unit suffix, a shortcut hint, or both.                                                                                                                                                                    |
| `trailing-text` | `in string`               | no default                        |                                                                                                                                                                                                                                     |
| `addon-align`   | `in InputGroupAddonAlign` | `InputGroupAddonAlign.inline-end` | Where `@children` sit inside the border.                                                                                                                                                                                            |
| `has-focus`     | `out bool`                | no default                        | Whether the group currently holds keyboard focus — also whether the focus ring is drawn, which is what makes the ring assertable.                                                                                                   |
| `scroll-offset` | `out length`              | no default                        | How far the text is scrolled under the group, as a non-positive offset. Zero until a multi-line group has stopped growing at its `max-height`.                                                                                      |

### Callbacks

| Callback           | Description                                                                                                          |
| ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `edited(string)`   | Fired on every keystroke with the current text.                                                                      |
| `accepted(string)` | Fired on Enter with the current text. A multi-line group breaks the line instead, the way any multi-line field does. |

### Enums

| Enum                   | Values                                                   |
| ---------------------- | -------------------------------------------------------- |
| `InputGroupVariant`    | `text`, `password`, `multi-line`                         |
| `InputGroupAddonAlign` | `inline-start`, `inline-end`, `block-start`, `block-end` |

## Accessibility

- **Unified text entry node.** Publishes a single `accessible-role: text-input` node with the current text and placeholder.
- **Contextual adornments.** Addon text (like units or URL schemes) remains accessible as supplementary context.
- **`invalid` announces “Invalid”; `loading` announces `loading-label`.** The loading label defaults to `"Loading"`, remains overridable and falls back to `"Busy"` when explicitly empty. Slint 1.17 publishes neither `accessible-invalid` nor a busy state, so both ride the description channel ADR-0013 opened; when both are present, the loading label is announced before `"Invalid"`.
- **Focus ring.** The focus ring envelops all welded addons, giving clear visual indication of active focus.
