Tag Group
A focusable list of tags with selection and removal.
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.
`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>.
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_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>.
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).
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>.
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).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
An element expression with window: &mut Window and cx: &mut App in scope.
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()])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.
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(),
])Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
An element expression with window: &mut Window and cx: &mut App in scope.
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()])An element expression with window: &mut Window and cx: &mut App in scope.
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()])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>.
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).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
Runs in a view's render: self is the view holding this example's state and cx is its Context<Self>.
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).
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>.
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).
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>.
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.
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(),
}
}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
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
| Method | Values | Description |
|---|---|---|
size(Size) | Size::Sm | Size::Md | Size::Lg | Sets padding, type size, and large-tag radius for every tag. |
variant(TagVariant) | TagVariant::Default | TagVariant::Surface | Selects the unselected background and hover token for every tag. |
// 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
| Builder | Values | Default | Description |
|---|---|---|---|
selection_mode( | SelectionMode:: | SelectionMode:: | Chooses inert, single, or multiple tag selection. |
selected_keys( | Selection | — | Controlled selected keys; the owner must accept reported changes. |
default_selected_keys( | Selection | — | Seeds the group's uncontrolled selected-key set once. |
on_selection_change( | Fn( | — | Reports the complete selected-key set after pointer and keyboard changes. |
disabled_keys( | iterable of keys | — | Removes matching tags from pointer and keyboard interaction. |
is_disabled( | true | | false | Disables every tag and remove action in the group. |
on_remove( | Fn( | — | Adds remove actions and reports one pointer key or the keyboard-selected set. |
size( | Size:: | Size:: | Sets padding, type size, and large-tag radius for every tag. |
variant( | TagVariant:: | TagVariant:: | Selects the unselected background and hover token for every tag. |
disallow_empty_selection( | true | | false | Inherited MultipleSelection policy blocks the final-key toggle and Escape clearing. |
escape_key_behavior( | EscapeKeyBehavior:: | EscapeKeyBehavior:: | Controls whether unmodified Escape clears a nonempty selection or remains available to an enclosing interaction. |
fixed release activationPartial | true | | false | GPUI activation occurs on click release and is not caller-configurable. |
new( | AnyElement | | — | The constructor collection plus label and description builders provide the compound content. |
new( | iterable of T | — | The constructor accepts the complete typed tag collection. |
empty_state(Partial | Fn( | — | A custom string replaces the built-in empty message, without list render props. |
new( | Key | — | Stable tag identity used for selection and removal callbacks. |
tag_content( | AnyElement | | — | Replacement content receives selected, disabled, hovered, pressed, focused, and focus-visible state. |
remove_content( | AnyElement | AnyElement:: | Each tag may replace the automatic close glyph with custom remove-button content while preserving the built-in action. |
Not ported8 HeroUI entries not ported
| HeroUI v3 | Why it is not ported |
|---|---|
TagGroup.selectionBehavior | Implements pinned toggle selection only. |
TagGroup.onAction | Tags expose selection and removal but no separate item action callback. |
TagGroup.errorMessage | Has no validation or error-message slot on TagGroup. |
TagGroup.className / render | Browser styles and root-element substitution are unavailable. |
TagGroup.List.className / render | The list part cannot be replaced or styled through browser classes. |
Tag.onAction | Exposes group selection and removal callbacks only. |
Tag.className / render | Attributes, link behavior, and element substitution are unavailable. |
Tag.RemoveButton.className | Browser class customization is unavailable. |
Parts
| Part | Description |
|---|---|
TagGroup | Field-like root owning selection, removal, label, and description. |
TagGroup | Wrapping horizontal list and empty-state owner, rendered inside the monolithic root. |
Tag | Focusable selectable item with optional prefix and caller-rendered content. |
Tag | Automatic per-tag remove action with replaceable caller content. |
States
| State | Builder | Description |
|---|---|---|
Selected | selected_keys | Uses accent-soft foreground and fill, including its selected hover token. |
Disabled | is_disabled | Dims the tag and removes selection, removal, and roving-focus behavior. |
Hovered | hover chrome | Selectable enabled tags use the variant-specific hover fill and expose hover render state. |
Pressed | interaction | The pinned stylesheet has no pressed visual rule; selectable enabled tags expose pressed render state. |
Focused | cursor keyed state | One enabled tag owns the group's roving focus handle. |
Focus visible | with_focus_ring | Keyboard focus draws the status ring and reaches tag render state. |