---
name: tag-group
title: Tag Group
url: https://ui.frontic.com/components/ui/tag-group.md
tier: ui
install: npx @frontic/ui add tag-group
files:
  - components/ui/tag-group/TagGroup.vue
  - components/ui/tag-group/TagGroupItem.vue
  - components/ui/tag-group/TagGroupItemDelete.vue
  - components/ui/tag-group/TagGroupItemText.vue
  - components/ui/tag-group/index.ts
dependencies:
  - "@lucide/vue"
  - "@vueuse/core"
  - reka-ui
---

# Tag Group

Tags the shopper moves through with the arrow keys, such as active filters to remove or options to pick; selection and removal are both optional.

## Install

```sh
npx @frontic/ui add tag-group
```

Used by: [Category Filter](https://ui.frontic.com/components/templates/category-filter.md), [Filter Sheet](https://ui.frontic.com/components/templates/filter-sheet.md), [Filter Sidebar](https://ui.frontic.com/components/templates/filter-sidebar.md), [Filter Toolbar](https://ui.frontic.com/components/templates/filter-toolbar.md)

npm packages: `@lucide/vue`, `@vueuse/core`, `reka-ui`

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { TagGroup, TagGroupItem, TagGroupItemDelete, TagGroupItemText } from '@/components/ui/tag-group'

const filters = ref(['Jackets', 'Northridge', 'Size M', 'Under €150', 'Waterproof'])

function remove(values: unknown[]) {
  filters.value = filters.value.filter((filter) => !values.includes(filter))
}
</script>

<template>
  <TagGroup aria-label="Active filters" @remove="remove">
    <TagGroupItem v-for="filter in filters" :key="filter" :value="filter">
      <TagGroupItemText>{{ filter }}</TagGroupItemText>
      <TagGroupItemDelete />
    </TagGroupItem>
  </TagGroup>
</template>
```

## Anatomy

```vue
<TagGroup>
  <TagGroupItem>
    <TagGroupItemText />
    <TagGroupItemDelete />
  </TagGroupItem>
</TagGroup>
```

## API reference

### TagGroup

Groups tags the user moves through with the arrow keys, and can select or remove. Renders a `<div>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `AcceptableValue \| AcceptableValue[]`: `null \| string \| number \| bigint \| Record<string, any> \| AcceptableValue[]` |  | The controlled value of the selected tag(s). Can be binded with `v-model`. An array when `selectionMode` is `multiple`. |
| `defaultValue` | `AcceptableValue \| AcceptableValue[]`: `null \| string \| number \| bigint \| Record<string, any> \| AcceptableValue[]` |  | The value of the selected tag(s) when initially rendered. Use when you do not need to control the selection. |
| `selectionMode` | `TagGroupSelectionMode`: `'single' \| 'multiple' \| 'none'` | `'none'` | The type of selection that is allowed. Tags are not selectable when `none`. |
| `disallowEmptySelection` | `boolean` | `false` | When `true`, the user cannot deselect the last selected tag. |
| `escapeKeyBehavior` | `'none' \| 'clearSelection'` | `'clearSelection'` | Whether pressing <kbd>Escape</kbd> clears the selection. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the tag group and all its tags. |
| `dir` | `Direction`: `'ltr' \| 'rtl'` |  | The reading direction of the tag group when applicable.<br><br>If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. |
| `loop` | `boolean` | `true` | When `true`, keyboard navigation will loop from last tag to first, and vice versa. |
| `by` | `string \| ((a: AcceptableValue, b: AcceptableValue) => boolean)`: `string \| (a: AcceptableValue, b: AcceptableValue): boolean` |  | Use this to compare objects by a particular field, or pass your own comparison function for complete control over how objects are compared. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `value: AcceptableValue \| AcceptableValue[] \| undefined` | Event handler called when the selection changes. |
| `remove` | `values: AcceptableValue[]` | Event handler called when the user removes tags, with the values to remove. Tags are only removable when this event has a listener; remove the values from your own list to remove the tags. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ modelValue: AcceptableValue \| AcceptableValue[] \| undefined; }` | Holds the tags; gets the selected value as `modelValue`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-empty` |  | Present while no option matches the search. |

### TagGroupItem

One tag: its text and, when removable, a delete button. Renders a `<div>`.

Styling: `tag-group-item`, which extends [`tag`](https://ui.frontic.com/DESIGN.md#tag). Change it there and every part that extends it follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `AcceptableValue`: `null \| string \| number \| bigint \| Record<string, any>` |  | The unique value of the tag. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the tag. |
| `textValue` | `string` |  | A string representation of the tag's contents, used for typeahead and as the tag's accessible name. Defaults to the text of `TagGroupItemText`. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'div'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ selected: boolean; disabled: boolean; }` | Holds the text and delete button; gets whether the tag is `selected` and `disabled`. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |
| `data-state` | `'checked' \| 'unchecked'` | The state the part is in, one value per state. |

### TagGroupItemDelete

Lets the user remove its tag; the group emits `remove`. Renders a `<button>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the delete button. |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'button'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Data attribute | Values | Description |
| --- | --- | --- |
| `data-disabled` |  | Present while the part is disabled. |

### TagGroupItemText

Shows the text of one tag. Renders a `<span>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `asChild` | `boolean` | `false` | Change the default rendered element for the one passed as a child, merging their props and behavior.<br><br>Read our [Composition](https://www.reka-ui.com/docs/guides/composition) guide for more details. |
| `as` | `AsTag \| Component`: `ComponentOptions<any, any, any, ComputedOptions, MethodOptions, any, any, any, string, {}, {}, string, {}, {}, {}, string, ComponentProvideOptions> \| (props: any, ctx: Omit<{ attrs: Attrs; slots: Readonly<InternalSlots>; emit: (event: string, ...args: any[]) => void; expose: <Exposed extends Record<string, any> = Record<string, any>>(exposed?: Exposed \| undefined) => void; }, 'expose'>): any \| ComponentPublicInstanceConstructor<any, any, any, any, ComputedOptions, MethodOptions> \| 'a' \| 'button' \| 'div' \| 'form' \| 'h2' \| 'h3' \| 'img' \| 'input' \| 'label' \| 'li' \| 'nav' \| 'ol' \| 'p' \| 'span' \| 'svg' \| 'ul' \| 'template' \| {} & string` | `'span'` | The element or component this component should render as. Can be overwritten by `asChild`. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

## Styling

| Part | Recipe |
| --- | --- |
| `default` | [`tag-group`](https://ui.frontic.com/DESIGN.md#tag-group) |
| `item` | [`tag-group-item`](https://ui.frontic.com/DESIGN.md#tag-group-item) |
| `item-delete` | [`tag-group-item-delete`](https://ui.frontic.com/DESIGN.md#tag-group-item-delete) |
| `focus-ring` | [`focus-ring`](https://ui.frontic.com/DESIGN.md#focus-ring) |
| `disabled` | [`disabled`](https://ui.frontic.com/DESIGN.md#disabled) |

States with a look of their own: hover, focus, checked and disabled.

## Source

`components/ui/tag-group/TagGroup.vue`

```vue
<script lang="ts">
/** Groups tags the user moves through with the arrow keys, and can select or remove. */
export default {}
</script>

<script setup lang="ts">
import type { AcceptableValue, TagGroupRootEmits, TagGroupRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TagGroupRoot, useForwardPropsEmits } from 'reka-ui'
import { computed, getCurrentInstance } from 'vue'

const props = defineProps<TagGroupRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<TagGroupRootEmits>()

const delegatedProps = reactiveOmit(props, 'class')

const forwarded = useForwardPropsEmits(delegatedProps, emits)

defineSlots<{
  /** Holds the tags; gets the selected value as `modelValue`. */
  default?: (props: { modelValue: AcceptableValue | AcceptableValue[] | undefined }) => unknown
}>()

// Reka lets tags be removed only when its root has a remove listener, so pass one on only when this group has one.
const instance = getCurrentInstance()
const bound = computed(() => {
  if (instance?.vnode.props?.onRemove) return forwarded.value
  const { onRemove: _, ...rest } = forwarded.value
  return rest
})
</script>

<template>
  <TagGroupRoot v-slot="slotProps" v-bind="bound" :class="['fui-tag-group outline-none', props.class]">
    <slot v-bind="slotProps" />
  </TagGroupRoot>
</template>
```

`components/ui/tag-group/TagGroupItem.vue`

```vue
<script lang="ts">
/** One tag: its text and, when removable, a delete button. */
export default {}
</script>

<script setup lang="ts">
import type { TagGroupItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TagGroupItem, useForwardProps } from 'reka-ui'

const props = defineProps<TagGroupItemProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)

defineSlots<{
  /** Holds the text and delete button; gets whether the tag is `selected` and `disabled`. */
  default?: (props: { selected: boolean; disabled: boolean }) => unknown
}>()
</script>

<template>
  <TagGroupItem
    v-slot="slotProps"
    v-bind="forwardedProps"
    :class="['fui-tag-group-item whitespace-nowrap outline-none select-none transition-colors focus-visible:fui-focus-ring data-disabled:pointer-events-none data-disabled:fui-disabled', props.class]"
  >
    <slot v-bind="slotProps" />
  </TagGroupItem>
</template>
```

`components/ui/tag-group/TagGroupItemDelete.vue`

```vue
<script lang="ts">
/** Lets the user remove its tag; the group emits `remove`. */
export default {}
</script>

<script setup lang="ts">
import { XIcon } from '@lucide/vue'

import type { TagGroupItemDeleteProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TagGroupItemDelete, useForwardProps } from 'reka-ui'

const props = defineProps<TagGroupItemDeleteProps & { class?: HTMLAttributes['class'] }>()

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <TagGroupItemDelete v-bind="forwardedProps" :class="['fui-tag-group-item-delete bg-transparent outline-none focus-visible:fui-focus-ring data-disabled:pointer-events-none', props.class]">
    <slot>
      <XIcon class="size-3.5" />
    </slot>
  </TagGroupItemDelete>
</template>
```

`components/ui/tag-group/TagGroupItemText.vue`

```vue
<script lang="ts">
/** Shows the text of one tag. */
export default {}
</script>

<script setup lang="ts">
import type { TagGroupItemTextProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TagGroupItemText, useForwardProps } from 'reka-ui'

const props = withDefaults(defineProps<TagGroupItemTextProps & { class?: HTMLAttributes['class'] }>(), { as: 'span' })

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <TagGroupItemText v-bind="forwardedProps" :class="['truncate', props.class]">
    <slot />
  </TagGroupItemText>
</template>
```
