TagGroup
Groups selectable or removable tags.
Import
ts
import { Tag, TagGroup, TagRemoveButton } from '@heroui-vue/vue'Usage
Categories
News Travel Gaming Shopping
<template>
<TagGroup label="Categories" selection-mode="single" :default-selected-keys="['travel']">
<Tag value="news">
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M4 5h11a3 3 0 0 1 3 3v11H7a3 3 0 0 1-3-3V5Z" stroke="currentColor" stroke-linejoin="round" stroke-width="2" />
<path d="M8 9h6M8 13h6" stroke="currentColor" stroke-linecap="round" stroke-width="2" />
</svg>
News
</Tag>
<Tag value="travel">
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M3 11.5 21 4l-7.5 18-3-7.5L3 11.5Z" stroke="currentColor" stroke-linejoin="round" stroke-width="2" />
</svg>
Travel
</Tag>
<Tag value="gaming">
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M7 10h10a4 4 0 0 1 3.7 2.5l.6 1.5a3 3 0 0 1-4.7 3.4L14 15H10l-2.6 2.4A3 3 0 0 1 2.7 14l.6-1.5A4 4 0 0 1 7 10Z" stroke="currentColor" stroke-linejoin="round" stroke-width="2" />
<path d="M8 13h.01M16 13h.01" stroke="currentColor" stroke-linecap="round" stroke-width="2" />
</svg>
Gaming
</Tag>
<Tag value="shopping">
<svg aria-hidden="true" fill="none" viewBox="0 0 24 24">
<path d="M6 8h12l-1 12H7L6 8Z" stroke="currentColor" stroke-linejoin="round" stroke-width="2" />
<path d="M9 8a3 3 0 0 1 6 0" stroke="currentColor" stroke-linecap="round" stroke-width="2" />
</svg>
Shopping
</Tag>
</TagGroup>
</template>
<script setup lang="ts">
import { Tag, TagGroup } from '@heroui-vue/vue'
</script>Anatomy
vue
<TagGroup>
<Tag>
Label
<TagRemoveButton />
</Tag>
</TagGroup>Sizes
Small
NewsTravelGaming
Medium
NewsTravelGaming
Large
NewsTravelGaming
<template>
<div class="demo-tag-group-stack">
<TagGroup label="Small" selection-mode="multiple" size="sm" :default-selected-keys="['news']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
</TagGroup>
<TagGroup label="Medium" selection-mode="multiple" size="md" :default-selected-keys="['travel']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
</TagGroup>
<TagGroup label="Large" selection-mode="multiple" size="lg" :default-selected-keys="['gaming']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
</TagGroup>
</div>
</template>
<script setup lang="ts">
import { Tag, TagGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-tag-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.25rem;
}
</style>Variants
Default
VueReactSvelte
Surface
VueReactSvelte
<template>
<div class="demo-tag-group-stack">
<TagGroup label="Default" selection-mode="multiple" :default-selected-keys="['vue']">
<Tag value="vue">Vue</Tag>
<Tag value="react">React</Tag>
<Tag value="svelte">Svelte</Tag>
</TagGroup>
<TagGroup label="Surface" selection-mode="multiple" variant="surface" :default-selected-keys="['react']">
<Tag value="vue">Vue</Tag>
<Tag value="react">React</Tag>
<Tag value="svelte">Svelte</Tag>
</TagGroup>
</div>
</template>
<script setup lang="ts">
import { Tag, TagGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-tag-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.25rem;
}
</style>Disabled
Disabled keys
NewsTravelGamingShopping
Disabled group
NewsTravelGaming
<template>
<div class="demo-tag-group-stack">
<TagGroup
label="Disabled keys"
selection-mode="multiple"
:default-selected-keys="['news']"
:disabled-keys="['gaming', 'shopping']"
>
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
<Tag value="shopping">Shopping</Tag>
</TagGroup>
<TagGroup label="Disabled group" selection-mode="multiple" disabled :default-selected-keys="['travel']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
</TagGroup>
</div>
</template>
<script setup lang="ts">
import { Tag, TagGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-tag-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.25rem;
}
</style>Selection Modes
Single selection
NewsTravelGamingShopping
Multiple selection
NewsTravelGamingShopping
<template>
<div class="demo-tag-group-stack">
<TagGroup label="Single selection" selection-mode="single" :default-selected-keys="['travel']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
<Tag value="shopping">Shopping</Tag>
</TagGroup>
<TagGroup label="Multiple selection" selection-mode="multiple" :default-selected-keys="['news', 'gaming']">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
<Tag value="shopping">Shopping</Tag>
</TagGroup>
</div>
</template>
<script setup lang="ts">
import { Tag, TagGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-tag-group-stack {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1.25rem;
}
</style>Controlled
Interests
NewsTravelGamingShopping
Selected: travel, gaming
<template>
<div class="demo-tag-group-controlled">
<TagGroup v-model:selected-keys="selectedKeys" label="Interests" selection-mode="multiple">
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
<Tag value="shopping">Shopping</Tag>
</TagGroup>
<Description>Selected: {{ selectedLabel }}</Description>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Description, Tag, TagGroup } from '@heroui-vue/vue'
const selectedKeys = ref<Array<string | number>>(['travel', 'gaming'])
const selectedLabel = computed(() => selectedKeys.value.length ? selectedKeys.value.join(', ') : 'none')
</script>
<style lang="less">
.demo-tag-group-controlled {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
</style>With Error Message
<template>
<form class="demo-tag-group-form" @submit.prevent="handleSubmit">
<TagGroup
v-model:selected-keys="selectedKeys"
label="Choose at least one topic"
selection-mode="multiple"
:is-invalid="showError"
>
<Tag value="news">News</Tag>
<Tag value="travel">Travel</Tag>
<Tag value="gaming">Gaming</Tag>
<Tag value="shopping">Shopping</Tag>
</TagGroup>
<FieldError v-if="showError">Select at least one topic.</FieldError>
<Button type="submit">Submit</Button>
</form>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import { Button, FieldError, Tag, TagGroup } from '@heroui-vue/vue'
const selectedKeys = ref<Array<string | number>>([])
const showError = ref(false)
const handleSubmit = () => {
showError.value = selectedKeys.value.length === 0
}
watch(selectedKeys, (keys) => {
if (keys.length > 0) {
showError.value = false
}
})
</script>
<style lang="less">
.demo-tag-group-form {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.75rem;
}
</style>With Prefix
With Icons
News Travel Gaming Shopping
Tags with icons
With Avatars
F Fred
M Michael
J Jane
Tags with avatars
<template>
<div class="demo-tag-group-prefix">
<TagGroup label="With Icons" selection-mode="single">
<Tag value="news">
<svg aria-hidden="true" fill="none" viewBox="0 0 16 16">
<path clip-rule="evenodd" d="M11.5 3h-7A1.5 1.5 0 0 0 3 4.5v7A1.5 1.5 0 0 0 4.5 13h7a1.5 1.5 0 0 0 1.5-1.5v-7A1.5 1.5 0 0 0 11.5 3m-7-1.5a3 3 0 0 0-3 3v7a3 3 0 0 0 3 3h7a3 3 0 0 0 3-3v-7a3 3 0 0 0-3-3zm6 6H5.43a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h5.07a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1m-5.32-3h3.57a.75.75 0 0 1 0 1.5H5.18a.75.75 0 0 1 0-1.5" fill="currentColor" fill-rule="evenodd" />
</svg>
News
</Tag>
<Tag value="travel">
<svg aria-hidden="true" fill="none" viewBox="0 0 16 16">
<path clip-rule="evenodd" d="M8 13.5c1.63 0 3.094-.709 4.101-1.835A2.5 2.5 0 0 1 10.25 9.25V9a.75.75 0 0 0-.75-.75 2.25 2.25 0 0 1 0-4.5.75.75 0 0 0 .75-.75v-.02a5.5 5.5 0 0 0-7.471 3.287A2.25 2.25 0 0 1 4.75 8.5c0 .414.336.75.75.75a2.25 2.25 0 0 1 1.265 4.11q.597.139 1.235.14m-3.491-1.25H5.5a.75.75 0 0 0 0-1.5A2.25 2.25 0 0 1 3.25 8.5a.75.75 0 0 0-.744-.75 5.49 5.49 0 0 0 2.003 4.5m8.241-2h.27A5.5 5.5 0 0 0 13.5 8c0-1.665-.74-3.158-1.91-4.166A2.25 2.25 0 0 1 9.5 5.25a.75.75 0 1 0 0 1.5A2.25 2.25 0 0 1 11.75 9v.25a1 1 0 0 0 1 1M8 15A7 7 0 1 0 8 1a7 7 0 0 0 0 14" fill="currentColor" fill-rule="evenodd" />
</svg>
Travel
</Tag>
<Tag value="gaming">
<svg aria-hidden="true" fill="none" viewBox="0 0 16 16">
<g clip-path="url(#tag-prefix-rocket)">
<path clip-rule="evenodd" d="m6.993 5.628-.787-.157-1.118-.224a1.13 1.13 0 0 0-1.024.31L2.837 6.785a1.15 1.15 0 0 0-.285.474l1.02.185a6.19 6.19 0 0 1 4.985 4.985l.185 1.02c.178-.055.34-.152.474-.285l1.227-1.227c.268-.268.384-.652.31-1.024l-.224-1.118-.157-.787.567-.568 1.243-1.242A4.5 4.5 0 0 0 13.5 4.015V2.5h-1.515a4.5 4.5 0 0 0-3.182 1.318L7.561 5.061zM12 9.5l1.243-1.243A6 6 0 0 0 15 4.015V2.5A1.5 1.5 0 0 0 13.5 1h-1.515a6 6 0 0 0-4.242 1.757L6.5 4l-1.118-.224a2.63 2.63 0 0 0-2.379.72L1.777 5.724A2.65 2.65 0 0 0 1 7.598c0 .522.373.97.887 1.063l1.417.258a4.69 4.69 0 0 1 3.777 3.777l.258 1.417c.093.514.54.887 1.063.887.703 0 1.377-.28 1.875-.777l1.226-1.226a2.63 2.63 0 0 0 .72-2.38zm-8.06 2.571c-.311-.31-.76-.28-1.005-.036-.233.233-.423.658-.527 1.247a5 5 0 0 0-.05.366q.184-.019.377-.053c.596-.106 1.017-.296 1.24-.52.245-.244.275-.693-.036-1.004M5 11.011c-.873-.874-2.273-.89-3.126-.036C.777 12.07.802 14.094.837 14.712c.007.12.06.23.145.315a.5.5 0 0 0 .32.145c.622.032 2.652.046 3.734-1.035.853-.854.837-2.253-.036-3.126m6.78-5.73a.75.75 0 0 0-1.06-1.061l-1.5 1.5a.75.75 0 0 0 1.06 1.06z" fill="currentColor" fill-rule="evenodd" />
</g>
<defs>
<clipPath id="tag-prefix-rocket">
<path d="M0 0h16v16H0z" fill="currentColor" />
</clipPath>
</defs>
</svg>
Gaming
</Tag>
<Tag value="shopping">
<svg aria-hidden="true" fill="none" viewBox="0 0 16 16">
<path clip-rule="evenodd" d="M5.174 3h5.652a1.5 1.5 0 0 1 1.49 1.328l.808 7A1.5 1.5 0 0 1 11.634 13H4.366a1.5 1.5 0 0 1-1.49-1.672l.808-7A1.5 1.5 0 0 1 5.174 3m-2.98 1.156A3 3 0 0 1 5.174 1.5h5.652a3 3 0 0 1 2.98 2.656l.808 7a3 3 0 0 1-2.98 3.344H4.366a3 3 0 0 1-2.98-3.344zM5 5.25a.75.75 0 0 1 1.5 0v.25a1.5 1.5 0 1 0 3 0v-.25a.75.75 0 0 1 1.5 0v.25a3 3 0 0 1-6 0z" fill="currentColor" fill-rule="evenodd" />
</svg>
Shopping
</Tag>
<Description>Tags with icons</Description>
</TagGroup>
<TagGroup label="With Avatars" selection-mode="single">
<Tag value="fred">
<Avatar class="demo-tag-group-avatar" size="sm">
<AvatarImage src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg" alt="Fred" />
<AvatarFallback>F</AvatarFallback>
</Avatar>
Fred
</Tag>
<Tag value="michael">
<Avatar class="demo-tag-group-avatar" size="sm">
<AvatarImage src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg" alt="Michael" />
<AvatarFallback>M</AvatarFallback>
</Avatar>
Michael
</Tag>
<Tag value="jane">
<Avatar class="demo-tag-group-avatar" size="sm">
<AvatarImage src="https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg" alt="Jane" />
<AvatarFallback>J</AvatarFallback>
</Avatar>
Jane
</Tag>
<Description>Tags with avatars</Description>
</TagGroup>
</div>
</template>
<script setup lang="ts">
import { Avatar, AvatarFallback, AvatarImage, Description, Tag, TagGroup } from '@heroui-vue/vue'
</script>
<style lang="less">
.demo-tag-group-prefix {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 2rem;
}
.demo-tag-group-avatar {
width: 1rem;
height: 1rem;
[data-slot="avatar-fallback"] {
font-size: 0.625rem;
}
}
</style>With Remove Button
Removable tags
News Travel Gaming Shopping
<template>
<TagGroup
v-model:selected-keys="selectedKeys"
label="Removable tags"
selection-mode="multiple"
@remove="handleRemove"
>
<Tag v-for="tag in tags" :key="tag.id" :value="tag.id">
{{ tag.label }}
<TagRemoveButton />
</Tag>
</TagGroup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Tag, TagGroup, TagRemoveButton } from '@heroui-vue/vue'
const tags = ref([
{ id: 'news', label: 'News' },
{ id: 'travel', label: 'Travel' },
{ id: 'gaming', label: 'Gaming' },
{ id: 'shopping', label: 'Shopping' },
])
const selectedKeys = ref<Array<string | number>>(tags.value.map((tag) => tag.id))
const handleRemove = (key: string | number) => {
tags.value = tags.value.filter((tag) => tag.id !== key)
}
</script>With List Data
Team members
F Fred
M Michael
J Jane
A Alice
B Bob
C Charlie
Select team members for your project
Selected:
<template>
<div class="demo-tag-group-list-data">
<TagGroup v-model:selected-keys="selectedKeys" label="Team members" selection-mode="multiple">
<Tag v-for="member in members" :key="member.id" :value="member.id" variant="surface">
<Avatar class="demo-tag-group-avatar" size="sm">
<AvatarImage :src="member.avatar" :alt="member.name" />
<AvatarFallback>{{ member.fallback }}</AvatarFallback>
</Avatar>
{{ member.name }}
</Tag>
</TagGroup>
<Description>Select team members for your project</Description>
<div v-if="selectedMembers.length" class="demo-tag-group-selected">
<p>Selected:</p>
<div class="demo-tag-group-selected__list">
<div v-for="member in selectedMembers" :key="`${member.id}-selected`" class="demo-tag-group-selected__item">
<Avatar class="demo-tag-group-avatar" size="sm">
<AvatarImage :src="member.avatar" :alt="member.name" />
<AvatarFallback>{{ member.fallback }}</AvatarFallback>
</Avatar>
<span>{{ member.name }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { Avatar, AvatarFallback, AvatarImage, Description, Tag, TagGroup } from '@heroui-vue/vue'
const members = [
{ id: 'fred', name: 'Fred', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/blue.jpg', fallback: 'F' },
{ id: 'michael', name: 'Michael', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/green.jpg', fallback: 'M' },
{ id: 'jane', name: 'Jane', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/purple.jpg', fallback: 'J' },
{ id: 'alice', name: 'Alice', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/red.jpg', fallback: 'A' },
{ id: 'bob', name: 'Bob', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/orange.jpg', fallback: 'B' },
{ id: 'charlie', name: 'Charlie', avatar: 'https://heroui-assets.nyc3.cdn.digitaloceanspaces.com/avatars/black.jpg', fallback: 'C' },
]
const selectedKeys = ref<Array<string | number>>(['fred', 'michael'])
const selectedMembers = computed(() => members.filter((member) => selectedKeys.value.includes(member.id)))
</script>
<style lang="less">
.demo-tag-group-list-data {
width: 24rem;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
}
.demo-tag-group-avatar {
width: 1rem;
height: 1rem;
[data-slot="avatar-fallback"] {
font-size: 0.625rem;
}
}
.demo-tag-group-selected {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-top: 0.5rem;
p {
margin: 0;
color: var(--color-muted);
font-size: 0.875rem;
font-weight: 500;
}
}
.demo-tag-group-selected__list {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.demo-tag-group-selected__item {
display: flex;
align-items: center;
gap: 0.5rem;
border-radius: 0.5rem;
background: var(--color-surface-tertiary);
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
</style>Custom Render Function
Status
Online Away Busy Offline
<template>
<TagGroup
v-model:selected-keys="selectedKeys"
class="demo-tag-group-custom"
label="Status"
selection-mode="single"
variant="surface"
>
<Tag v-for="status in statuses" :key="status.id" :value="status.id">
<span class="demo-tag-group-custom__dot" :style="{ backgroundColor: status.color }" />
{{ status.label }}
</Tag>
</TagGroup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { Tag, TagGroup } from '@heroui-vue/vue'
const selectedKeys = ref<Array<string | number>>(['online'])
const statuses = [
{ id: 'online', label: 'Online', color: '#17c964' },
{ id: 'away', label: 'Away', color: '#f5a524' },
{ id: 'busy', label: 'Busy', color: '#f31260' },
{ id: 'offline', label: 'Offline', color: '#71717a' },
]
</script>
<style lang="less">
.demo-tag-group-custom {
align-items: flex-start;
}
.demo-tag-group-custom__dot {
width: 0.5rem;
height: 0.5rem;
border-radius: 999px;
}
</style>Related Components
Styling
TagGroup owns the label, list layout, selection state, disabled keys, and remove callbacks. Use component props for native tag behavior first. Demo-only layout styles should stay in each demo file and be shown in the source panel.
API
| Prop | Type | Default | Description |
|---|---|---|---|
selectionMode | 'none' | 'single' | 'multiple' | 'none' | Enables single or multiple tag selection |
selectedKeys | Array<string | number> | undefined | Controlled selected keys |
defaultSelectedKeys | Array<string | number> | [] | Initial uncontrolled selected keys |
disabledKeys | Array<string | number> | [] | Keys that cannot be selected |
disabled / isDisabled | boolean | false | Disables the whole group |
isInvalid | boolean | false | Marks the group and child tags invalid |
size | 'sm' | 'md' | 'lg' | 'md' | Size inherited by child tags |
variant | 'default' | 'surface' | 'default' | Visual style inherited by child tags |
| Event | Payload | Description |
|---|---|---|
update:selectedKeys | Array<string | number> | Emitted when selection changes |
selection-change | Array<string | number> | Emitted when selection changes |
remove | string | number | Emitted when a tag remove button removes a tag key |