Skip to content
PorabuildHeroGPUI component library
v0.14.0 · Apache-2.0

Collections

Tag Group

A focusable list of tags with selection and removal.

Rust
use herogpui::components::tag_group::{Tag, TagGroup};

TagGroup on docs.rsSource at v0.14.0

Usage

Showing Usage, live: HeroGPUI compiled to WebAssembly. The canvas has no accessibility tree; each example's description and code below carry the same information.

Usage

`radius(px)` replaces the chip's size-step corner. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new("tag-group", tags())
        .full_width(true)
        .label("Skills")
        .radius(px(2.))
        .into_any_element()])

Also calls title_case (included when you copy).

Hover Colour

`hover_bg` names a hovered tag's fill and `remove_hover_bg` the remove button's, which stays a circle. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
use std::collections::HashSet;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new("tag-group-hover-bg", tags())
        .label("Skills")
        .selection_mode(SelectionMode::Single)
        .hover_bg(cx.colors().accent.soft())
        .remove_hover_bg(cx.colors().accent.soft_hover())
        .on_remove(cx.listener(
            |this, keys: &HashSet<SharedString>, _, cx| {
                this.tags.retain(|key| !keys.contains(key));
                this.tag_selection.retain(|key| !keys.contains(key));
                cx.notify();
            },
        ))
        .into_any_element()])

Also calls title_case (included when you copy).

Transitions

Enabled tags ease their background between resting and hover over the 100ms smooth transition; selection keeps the same stable chip geometry. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-transitions", tags())
            .label("Hover or select a skill")
            .selection_mode(SelectionMode::Single)
            .default_selected_keys([SharedString::from("rust")])
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Disabled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-disabled", tags())
            .label("Skills")
            .is_disabled(true)
            .into_any_element(),
        TagGroup::new("tag-group-disabled-keys", tags())
            .label("Some disabled")
            // `disabledKeys` disables individual tags rather
            // than the whole group.
            .disabled_keys([SharedString::from("rust")])
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Selection Modes

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-single", tags())
            .selection_mode(SelectionMode::Single)
            .into_any_element(),
        TagGroup::new("tag-group-multiple", tags())
            .selection_mode(SelectionMode::Multiple)
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Controlled

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{ActiveTheme, SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{App, SharedString, px};
use herogpui::gpui;
use std::collections::HashSet;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};
let selection = self.tag_selection.clone();
let tag_selection = selection.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-controlled", tags())
            .selection_mode(SelectionMode::Multiple)
            .selected_keys(tag_selection.iter().cloned())
            .on_selection_change(cx.listener(
                |this, keys: &HashSet<SharedString>, _, cx| {
                    this.tag_selection = keys.clone();
                    cx.notify();
                },
            ))
            .into_any_element(),
        para(&format!("{} selected", tag_selection.len()), cx),
    ])

Also calls title_case, para (included when you copy).

Disallow Empty Selection

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new(
        "tag-group-disallow-empty",
        vec![Tag::new("design", "Design"), Tag::new("code", "Code")],
    )
    .label("At least one skill")
    .selection_mode(SelectionMode::Single)
    .default_selected_keys([SharedString::from("design")])
    .disallow_empty_selection(true)
    .into_any_element()])

Escape Key Behavior

With `None`, Escape preserves the selected tag and bubbles to any enclosing interaction that handles it. An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{EscapeKeyBehavior, SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new(
            "tag-group-escape-none",
            vec![Tag::new("design", "Design"), Tag::new("code", "Code")],
        )
        .label("Skills")
        .selection_mode(SelectionMode::Single)
        .default_selected_keys([SharedString::from("design")])
        .escape_key_behavior(EscapeKeyBehavior::None)
        .into_any_element(),
    ])

With Error Message

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new("tag-group-error", tags())
        .label("Skills")
        .description("Pick at least one")
        .into_any_element()])

Also calls title_case (included when you copy).

With List Data

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new(
        "tag-group-list",
        ["Design", "Research", "Writing", "Support", "Ops"]
            .into_iter()
            .map(|name| Tag::new(name.to_lowercase(), name))
            .collect(),
    )
    .label("Teams")
    .into_any_element()])

With Prefix

An element expression with window: &mut Window and cx: &mut App in scope.

Rust
use herogpui::prelude::{Tag, TagGroup, icons};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new(
        "tag-group-prefix",
        vec![
            Tag::new("rust", "Rust").icon(icons::CHECK),
            Tag::new("gpui", "GPUI").icon(icons::CHECK),
        ],
    )
    .label("Verified")
    .into_any_element()])

With Remove Button

The 24px remove target extends around the 12px icon without changing tag spacing. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
use std::collections::HashSet;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-remove-button", tags())
            .label("Skills")
            .on_remove(cx.listener(
                |this, keys: &HashSet<SharedString>, _, cx| {
                    this.tags.retain(|key| !keys.contains(key));
                    this.tag_selection.retain(|key| !keys.contains(key));
                    cx.notify();
                },
            ))
            .into_any_element(),
        {
            let custom_tags: Vec<Tag> = tags()
                .into_iter()
                .map(|tag| {
                    tag.remove_content(|| {
                        gpui::div().child("−").into_any_element()
                    })
                })
                .collect();
            TagGroup::new("tag-group-custom-remove-button", custom_tags)
                .label("Skills")
                .on_remove(cx.listener(
                    |this, keys: &HashSet<SharedString>, _, cx| {
                        this.tags.retain(|key| !keys.contains(key));
                        this.tag_selection.retain(|key| !keys.contains(key));
                        cx.notify();
                    },
                ))
        }
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Removable

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
use std::collections::HashSet;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new("tag-group-remove", tags())
        .label("Team")
        .description("Remove a tag to see the group update.")
        .empty_state("All tags removed")
        .on_remove(cx.listener(|this, keys: &HashSet<SharedString>, _, cx| {
            this.tags.retain(|key| !keys.contains(key));
            this.tag_selection.retain(|key| !keys.contains(key));
            cx.notify();
        }))
        .into_any_element()])

Also calls title_case (included when you copy).

Selectable

Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{SelectionMode, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::{SharedString, px};
use herogpui::gpui;
use std::collections::HashSet;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};
let selection = self.tag_selection.clone();

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![TagGroup::new("tag-group-select", tags())
        .selection_mode(SelectionMode::Multiple)
        .selected_keys(selection.iter().cloned())
        .on_selection_change(cx.listener(
            |this, keys: &HashSet<SharedString>, _, cx| {
                this.tag_selection = keys.clone();
                cx.notify();
            },
        ))
        .into_any_element()])

Also calls title_case (included when you copy).

Sizes

Small and medium tags use 16px line boxes; large tags use 20px, independent of inherited text styles. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Size, Tag, TagGroup};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-sm", tags())
            .size(Size::Sm)
            .into_any_element(),
        TagGroup::new("tag-group-md", tags())
            .size(Size::Md)
            .into_any_element(),
        TagGroup::new("tag-group-lg", tags())
            .size(Size::Lg)
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Variants

The default variant draws each tag as its own bordered pill; the surface variant tints the tag group so it reads inside a panel. Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.

Rust
use herogpui::prelude::{Tag, TagGroup, TagVariant};
use herogpui::gpui::prelude::*;
use herogpui::gpui::px;
use herogpui::gpui;

let tag_keys = self.tags.clone();
let tags = move || -> Vec<Tag> {
    tag_keys
        .iter()
        .map(|k| Tag::new(k.clone(), title_case(k)))
        .collect()
};

gpui::div()
    .flex()
    .flex_col()
    .items_start()
    .justify_center()
    .gap(px(12.))
    .children(vec![
        TagGroup::new("tag-group-default", tags()).into_any_element(),
        TagGroup::new("tag-group-surface", tags())
            .variant(TagVariant::Surface)
            .into_any_element(),
    ])

Also calls title_case (included when you copy).

Helpers used by these examples

Plain functions over the public API, defined once here and copied with each example that calls them.

title_case
fn title_case(s: &str) -> String {
    let mut chars = s.chars();
    match chars.next() {
        Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
        None => String::new(),
    }
}
para
fn para(text: &str, cx: &App) -> gpui::AnyElement {
    let colors = cx.colors();
    gpui::div()
        .text_size(px(14.5))
        .line_height(px(24.))
        .text_color(colors.foreground)
        .max_w(px(720.))
        .child(text.to_owned())
        .into_any_element()
}

Anatomy

Rust types

TagGroupTag

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

Customization

Shared appearance belongs on ThemeBuilder::components and .recipe("name"). Typed builders change how Tag Group looks at one call site. Every builder also carries sx for placement, width and flex: it takes GPUI's own styling methods and refines them over the component's root after the theme, so an instance override wins. Behaviour builders live in the API reference below.

Styling

Tag Group styling
MethodValuesDescription
size(Size)Size::Sm | Size::Md | Size::LgSets padding, type size, and large-tag radius for every tag.
variant(TagVariant)TagVariant::Default | TagVariant::SurfaceSelects the unselected background and hover token for every tag.
Rust
// The one slot for caller-owned low-level styling: GPUI's
// styling methods, refined over the root element last.
.sx(|el| {
    el.bg(gpui::rgba(0xffa500ff))             // background
        .text_color(gpui::rgba(0x000000ff))   // text
        .w(gpui::px(13.))                     // size
        .h(gpui::px(12.))
})

API reference

Against HeroUI v3.2.6: 28 entries ported, 3 partial, 8 not ported. Partial rows are marked in the tables; entries not ported are listed under each table with the reason.

Builders

Tag Group builders
BuilderValuesDefaultDescription
selection_mode(SelectionMode)SelectionMode::None | SelectionMode::Single | SelectionMode::MultipleSelectionMode::NoneChooses inert, single, or multiple tag selection.
selected_keys(keys)Selection—Controlled selected keys; the owner must accept reported changes.
default_selected_keys(keys)Selection—Seeds the group's uncontrolled selected-key set once.
on_selection_change(callback)Fn(keys: Selection) -> ()—Reports the complete selected-key set after pointer and keyboard changes.
disabled_keys(keys)iterable of keys—Removes matching tags from pointer and keyboard interaction.
is_disabled(bool)true | falsefalseDisables every tag and remove action in the group.
on_remove(callback)Fn(keys: Set<Key>) -> ()—Adds remove actions and reports one pointer key or the keyboard-selected set.
size(Size)Size::Sm | Size::Md | Size::LgSize::MdSets padding, type size, and large-tag radius for every tag.
variant(TagVariant)TagVariant::Default | TagVariant::SurfaceTagVariant::DefaultSelects the unselected background and hover token for every tag.
disallow_empty_selection(bool)true | falsefalseInherited MultipleSelection policy blocks the final-key toggle and Escape clearing.
escape_key_behavior(EscapeKeyBehavior)EscapeKeyBehavior::ClearSelection | EscapeKeyBehavior::NoneEscapeKeyBehavior::ClearSelectionControls whether unmodified Escape clears a nonempty selection or remains available to an enclosing interaction.
fixed release activationPartialtrue | falsefalseGPUI activation occurs on click release and is not caller-configurable.
new(id, tags) / label / descriptionAnyElement | render closure—The constructor collection plus label and description builders provide the compound content.
new(id, tags)iterable of T—The constructor accepts the complete typed tag collection.
empty_state(text)PartialFn(props: TagListRenderProps) -> AnyElement—A custom string replaces the built-in empty message, without list render props.
new(key, label)Key—Stable tag identity used for selection and removal callbacks.
tag_content(render)AnyElement | render closure—Replacement content receives selected, disabled, hovered, pressed, focused, and focus-visible state.
remove_content(render)AnyElementAnyElement::CloseIconEach tag may replace the automatic close glyph with custom remove-button content while preserving the built-in action.
Not ported8 HeroUI entries not ported
Tag Group builders not ported
HeroUI v3Why it is not ported
TagGroup.selectionBehaviorImplements pinned toggle selection only.
TagGroup.onActionTags expose selection and removal but no separate item action callback.
TagGroup.errorMessageHas no validation or error-message slot on TagGroup.
TagGroup.className / renderBrowser styles and root-element substitution are unavailable.
TagGroup.List.className / renderThe list part cannot be replaced or styled through browser classes.
Tag.onActionExposes group selection and removal callbacks only.
Tag.className / renderAttributes, link behavior, and element substitution are unavailable.
Tag.RemoveButton.classNameBrowser class customization is unavailable.

Parts

Tag Group parts
PartDescription
TagGroupField-like root owning selection, removal, label, and description.
TagGroupWrapping horizontal list and empty-state owner, rendered inside the monolithic root.
TagFocusable selectable item with optional prefix and caller-rendered content.
TagAutomatic per-tag remove action with replaceable caller content.

States

Tag Group states
StateBuilderDescription
Selectedselected_keysUses accent-soft foreground and fill, including its selected hover token.
Disabledis_disabledDims the tag and removes selection, removal, and roving-focus behavior.
Hoveredhover chromeSelectable enabled tags use the variant-specific hover fill and expose hover render state.
PressedinteractionThe pinned stylesheet has no pressed visual rule; selectable enabled tags expose pressed render state.
Focusedcursor keyed stateOne enabled tag owns the group's roving focus handle.
Focus visiblewith_focus_ringKeyboard focus draws the status ring and reaches tag render state.