Forms

Input Group

Combine an input with adjacent addons and controls.

Rust
1use herogpui::components::input_group::InputGroup;

Usage

Prefix and suffix text uses 14px text with 20px lines alongside the input.

Rust
1use herogpui::prelude::{Input, InputAddon, InputGroup};
2
3InputGroup::new()
4    .label("Website")
5    .prefix(InputAddon::new("https://"))
6    .input(
7        Input::new(self.demo_text("ig-usage", "", cx))
8            // v3's group example seeds the input with
9            // `defaultValue`; `value` is the controlled
10            // spelling of the same thing.
11            .default_value("heroui.com")
12            .placeholder("heroui.com"),
13    )

Live HeroGPUI compiled to WebAssembly. Select any example without loading another WASM instance.

Anatomy

Rust types

InputGroup

Input Group is assembled from these builders. The API reference lists each one.

Customization

Appearance builders and theme tokens Input Group uses.

Styling

Input Group styling
StyleDescription
text_size + line_heightPrefix and suffix text uses 14px text with 20px lines alongside the input.
div flex + min_h(FIELD_HEIGHT) + FIELD_TEXT + apply_field_chromeThe 36px floor, field radius, fill, 14px type, foreground and shadow match; GPUI has no inline-flex display mode, so the box leans on its parent for intrinsic width.
group.hover(field.hover() / default.hover() + border_hover()) when !focus_within && !is_disabledThe pinned hover-only group state, suppressed while the focus is inside; a disabled group paints none.
items_start + min_h(FIELD_HEIGHT) onlyA textarea group top-aligns its children and grows around the multi-line field instead of centring it.
apply_field_chrome(focus_within)The two-pixel focus ring matches; keeps the variant fill rather than switching to the field-focus fill.
apply_field_chrome(is_invalid)Danger outline and focused danger ring match; the field-focus fill swap is not drawn.
is_disabled propagation + group-box opacity + Label::is_disabledOne dim covers the group box, so field, addon slots and composed children dim exactly once — the propagated field skips its own dim; the folded label dims itself. Arbitrary children only dim: v3's hit testing: none has no GPUI analogue.
Input::in_group + flex_1 + pl/pr(12px)The held Input supplies the transparent fill, 12px inset, placeholder colour and unified 14px field type, dropping the padding on an addon side.
multiline min_h per rows_height line + 16px, no 38px floorThe rows math matches (one 20px line each over 16px of vertical padding), but the 38px one-row floor does not: rows(1) stands 36px. Browser resizing has no analogue.
InputAddon px(12px) + flex_shrink_0 + placeholder colorThe 12px inset, placeholder colour and flush edges match; the addons' own side borders are not drawn.
addon_slot pt(8px) under items_startAddon slots top-align with the 8px top padding, level with the textarea's first line.
apply_field_chrome(FieldVariant::Secondary) + default colorsResting fill and dropped shadow match, and the hover swap reaches the group.
full_width(bool) + root/group w_fullThe width lands on the outer wrapper and the group box, so the stretch is visible in an items-start parent.

API reference

Builders

Input Group builders
BuilderTypeDefaultDescription
input(Input) / text_area(TextArea) / child(AnyElement)Input / TextArea / AnyElementArbitrary children render after the field, and the typed slots compose the parts; the children-as-a-function form has no analogue, holds one field slot, and a disabled group dims these children without being able to make them pointer- or tab-inert.
full_width(bool)boolfalseThe width lands on the outer wrapper as well as the group box: gpui resolves a percentage against the parent, and a content-sized parent stretches nothing.
variant(FieldVariant)FieldVariantFieldVariant::PrimaryPrimary carries the field shadow; secondary drops it and paints the neutral default palette.
is_disabled(bool) + Input::is_disabledboolfalseTakes this from the TextField around the group; folds it onto InputGroup and propagates it to the held field, so a group-disabled control tracks no focus and answers no keys, while one dim covers the whole box.
is_invalid(bool)boolfalseThe pinned TextField composition owns the flag; folds it on and drives the group's invalid chrome with it.
is_required(bool)boolfalseMarks the folded label as required.
label(SharedString)SharedStringComposes Label as a sibling; folds it above the group box.
description(SharedString)SharedStringComposes Description as a sibling; folds it below the group box.
error_message(SharedString)SharedStringShown only while the group is invalid, as the TextField does.
input(Input) + Input::variant'primary' | 'secondary''primary'The group paints the shared chrome, so the held field's own variant flag is shadowed by the group's; the field accepts it through its builder.
input(Input) + Input::input_typestring'text'Password masking and numeric filtering ride on the held field.
input(Input) + InputState::valuestringThe controlled value is the bound InputState's text.
input(Input) + Input::default_valuestringSeeds the held field's state once.
input(Input) + Input::placeholderstringPlaceholder of the held field.
input(Input) + Input::is_read_onlyboolRead-only keeps the field focusable while edits are blocked.
text_area(TextArea) + TextArea::rowsnumberVisible line count; three 20px lines plus 16px vertical padding per the field's own rows math.
text_area(TextArea) + Input::variant'primary' | 'secondary''primary'The group paints the shared chrome, so the multi-line field's own variant flag is shadowed by the group's; the field accepts it through its builder.
text_area(TextArea) + InputState::valuestringThe controlled value is the bound InputState's text.
text_area(TextArea) + Input::default_valuestringSeeds the held field's state once.
text_area(TextArea) + Input::placeholderstringPlaceholder of the held field.
text_area(TextArea) + Input::is_read_onlyboolRead-only keeps the field focusable while edits are blocked.
prefix(AnyElement)AnyElementLeading addon content: text, icons or anything else, drawn in placeholder colour.
suffix(AnyElement)AnyElementTrailing addon content: text, buttons, badges or keyboards, keeping their own handlers.

Parts

Input Group parts
PartDescription
InputGroupThe shared field chrome box: variant palette, focus-within ring, hover fill, invalid chrome, one disabled dim over the box, and the handleClick focus transfer to the held field.
InputGroupThe caller's Input rendered chrome-less: transparent, unrounded, flex_1 between the addons, with the padding dropped on whichever side an addon occupies.
InputGroupThe same shared chrome around a multi-line field; its presence switches the group to items-start with auto height and 8px addon top padding.
InputGroupLeading addon slot in placeholder colour with a 12px inset; a click on it is the group root's business, which focuses the held field.
InputGroupTrailing addon slot; a focusable suffix (a copy Button) keeps its own press, and the group's click-to-focus hands the focus back to the field afterwards.

States

Input Group states
BuilderStateDescription
field.hover() / default.hover() + border_hover() when !focus_within && !is_disabledHoverHovering paints bg-field-hover with --field-border-hover, and secondary swaps only the fill for --default-hover; the refinement is baked off while the focus is inside, and a disabled group paints none (status-disabled is hit testing: none first).
apply_field_chrome(focus_within)Focus WithinThe held field's focus drives the group's status-focused-field ring; the --field-focus fill and border swap stay with the variant fill instead.
apply_field_chrome(is_invalid)InvalidStatus-invalid-field draws the danger outline and focused danger ring; the --field-focus fill swap stays with the variant fill.
is_disabled + Input::is_disabled + group-box opacity + Label::is_disabledDisabledThe flag propagates to the held field, which stops tracking focus and answering keys, and one dim covers the whole group box — field, addon slots and any composed children; the folded label dims itself beside it. Only the held field is truly inert: GPUI cannot make arbitrary children pointer- or tab-inert, so a composed button in a disabled group still answers.