---
name: tags-input
title: Tags Input
url: https://ui.frontic.com/components/ui/tags-input.md
tier: ui
install: npx @frontic/ui add tags-input
files:
  - components/ui/tags-input/TagsInput.vue
  - components/ui/tags-input/TagsInputInput.vue
  - components/ui/tags-input/TagsInputItem.vue
  - components/ui/tags-input/TagsInputItemDelete.vue
  - components/ui/tags-input/TagsInputItemText.vue
  - components/ui/tags-input/index.ts
registryDependencies:
  - use-field-control
dependencies:
  - "@lucide/vue"
  - "@vueuse/core"
  - reka-ui
---

# Tags Input

An underlined field that collects several short values as removable tags, such as materials or interests; joins a Field around it.

## Install

```sh
npx @frontic/ui add tags-input
```

Built from: [Use Field Control](https://ui.frontic.com/composables/use-field-control.md)

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

## Examples

```vue
<script setup lang="ts">
import { ref } from 'vue'
import { TagsInput, TagsInputInput, TagsInputItem, TagsInputItemDelete, TagsInputItemText } from '@/components/ui/tags-input'

const tags = ref(['Cotton', 'Linen'])
</script>

<template>
  <TagsInput v-model="tags">
    <TagsInputItem v-for="tag in tags" :key="tag" :value="tag">
      <TagsInputItemText />
      <TagsInputItemDelete />
    </TagsInputItem>
    <TagsInputInput placeholder="Add a material" />
  </TagsInput>
</template>
```

## Anatomy

```vue
<TagsInput>
  <TagsInputItem>
    <TagsInputItemText />
    <TagsInputItemDelete />
  </TagsInputItem>
  <TagsInputInput />
</TagsInput>
```

## API reference

### TagsInput

Groups all parts of the tags input, where the user enters several short values.

Styling: `tags-input`, which extends [`control`](https://ui.frontic.com/DESIGN.md#control) and [`control-line`](https://ui.frontic.com/DESIGN.md#control-line). Change them there and every part that extends them follows.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `modelValue` | `AcceptableInputValue[] \| null`: `null \| AcceptableInputValue[]` |  | The controlled value of the tags input. Can be bind as `v-model`. |
| `defaultValue` | `AcceptableInputValue[]` |  | The value of the tags that should be added. Use when you do not need to control the state of the tags input |
| `addOnPaste` | `boolean` | `false` | When `true`, allow adding tags on paste. Work in conjunction with delimiter prop. |
| `addOnTab` | `boolean` | `false` | When `true` allow adding tags on tab keydown |
| `addOnBlur` | `boolean` | `false` | When `true` allow adding tags blur input |
| `duplicate` | `boolean` | `false` | When `true`, allow duplicated tags. |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the tags input. |
| `delimiter` | `string \| RegExp` | `','` | The character or regular expression to trigger the addition of a new tag. Also used to split tags for `@paste` event |
| `dir` | `Direction`: `'ltr' \| 'rtl'` |  | The reading direction of the combobox when applicable.<br><br>If omitted, inherits globally from `ConfigProvider` or assumes LTR (left-to-right) reading mode. |
| `max` | `number` | `0` | Maximum number of tags. |
| `id` | `string` |  | The element's `id`, so a `<label for>` can name it. |
| `convertValue` | `((value: string) => AcceptableInputValue)`: `(value: string): AcceptableInputValue` |  | Convert the input value to the desired type. Mandatory when using objects as values and using `TagsInputInput` |
| `displayValue` | `((value: AcceptableInputValue) => string)`: `(value: AcceptableInputValue): string` |  | Display the value of the tag. Useful when you want to apply modifications to the value like adding a suffix or when using object as values |
| `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` |  | The element or component this component should render as. Can be overwritten by `asChild`. |
| `name` | `string` |  | The name of the field. Submitted with its owning form as part of a name/value pair. |
| `required` | `boolean` | `false` | When `true`, indicates that the user must set the value before the owning form can be submitted. |
| `class` | `ClassValue` |  | Classes added to the part's own element, after its recipe classes. |

| Event | Payload | Description |
| --- | --- | --- |
| `update:modelValue` | `payload: AcceptableInputValue[]` | Event handler called when the value changes |
| `invalid` | `payload: AcceptableInputValue` | Event handler called when the value is invalid |
| `addTag` | `payload: AcceptableInputValue` | Event handler called when tag is added |
| `removeTag` | `payload: AcceptableInputValue` | Event handler called when tag is removed |

| Slot | Slot props | Description |
| --- | --- | --- |
| `default` | `{ modelValue: AcceptableInputValue[]; }` | Holds the tags and the input; gets the current tags as `modelValue`. |

### TagsInputInput

Lets the user type a new tag and add it to the tags input. Renders a `<input>`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `placeholder` | `string` |  | The placeholder character to use for empty tags input. |
| `autoFocus` | `boolean` | `false` | Focus on element when mounted. |
| `maxLength` | `number` |  | Maximum number of character allowed. |
| `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` | `'input'` | 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-invalid` |  | Present while the value is invalid. |

### TagsInputItem

Groups the text and delete button of one tag.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `value` (required) | `AcceptableInputValue`: `string \| number \| bigint \| Record<string, any>` |  | Value associated with the tags |
| `disabled` | `boolean` | `false` | When `true`, prevents the user from interacting with the tags input. |
| `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` |  | 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. |

### TagsInputItemDelete

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

| 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` | `'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. |
| `removeLabel` | `string` | `'Remove'` | Read before the tag's text, so the button is announced as e.g. "Remove Cotton". |

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

### TagsInputItemText

Shows the value 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` | [`tags-input`](https://ui.frontic.com/DESIGN.md#tags-input) |
| `input` | [`tags-input-input`](https://ui.frontic.com/DESIGN.md#tags-input-input) |
| `item` | [`tags-input-item`](https://ui.frontic.com/DESIGN.md#tags-input-item) |
| `item-text` | [`tags-input-item-text`](https://ui.frontic.com/DESIGN.md#tags-input-item-text) |
| `item-delete` | [`tags-input-item-delete`](https://ui.frontic.com/DESIGN.md#tags-input-item-delete) |

States with a look of their own: focus and invalid.

## Source

`components/ui/tags-input/TagsInput.vue`

```vue
<script lang="ts">
/** Groups all parts of the tags input, where the user enters several short values. */
export default {}
</script>

<script setup lang="ts">
import type { AcceptableInputValue, TagsInputRootEmits, TagsInputRootProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { TagsInputRoot, useForwardPropsEmits } from 'reka-ui'
import { useTemplateRef } from 'vue'
import { useFieldControl } from '@library/composables/use-field-control'

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

const delegatedProps = reactiveOmit(props, 'class')

const forwarded = useForwardPropsEmits(delegatedProps, emits)

defineSlots<{
  /** Holds the tags and the input; gets the current tags as `modelValue`. */
  default?: (props: { modelValue: AcceptableInputValue[] }) => unknown
}>()

// Delete once Reka's TagsInput reads the Field itself.
const { joined, report } = useFieldControl({
  props,
  forwarded,
  root: useTemplateRef<{ $el: HTMLElement }>('root'),
  element: (root) => root?.querySelector<HTMLElement>('input:not([data-hidden])'),
  id: () => props.id,
  aria: 'none',
})
</script>

<template>
  <TagsInputRoot ref="root" v-slot="slotProps" v-bind="joined" @update:model-value="report" :class="['fui-tags-input outline-none transition', props.class]">
    <slot v-bind="slotProps" />
  </TagsInputRoot>
</template>
```

`components/ui/tags-input/TagsInputInput.vue`

```vue
<script lang="ts">
/** Lets the user type a new tag and add it to the tags input. */
export default {}
</script>

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

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

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)

// Takes the label, description and state of a Field around the tags input. Delete once Reka's TagsInput reads the Field itself.
const field = injectFieldRootContext(null)
const fieldAttrs = computed(() =>
  field
    ? {
        'aria-labelledby': field.labelId.value,
        'aria-describedby': field.describedBy.value,
        'aria-invalid': (field.invalid.value && !field.disabled.value) || undefined,
      }
    : {},
)
</script>

<template>
  <TagsInputInput v-bind="{ ...fieldAttrs, ...forwardedProps }" :class="['fui-tags-input-input bg-transparent focus:outline-none', props.class]" />
</template>
```

`components/ui/tags-input/TagsInputItem.vue`

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

<script setup lang="ts">
import type { TagsInputItemProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'

import { reactiveOmit } from '@vueuse/core'
import { TagsInputItem, useForwardProps } from 'reka-ui'

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

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <TagsInputItem v-bind="forwardedProps" :class="['fui-tags-input-item', props.class]">
    <slot />
  </TagsInputItem>
</template>
```

`components/ui/tags-input/TagsInputItemDelete.vue`

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

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

import type { TagsInputItemDeleteProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { injectTagsInputItemContext, TagsInputItemDelete, useForwardProps } from 'reka-ui'
import { useId } from 'vue'

const props = withDefaults(
  defineProps<
    TagsInputItemDeleteProps & {
      class?: HTMLAttributes['class']
      /** Read before the tag's text, so the button is announced as e.g. "Remove Cotton". */
      removeLabel?: string
    }
  >(),
  { removeLabel: 'Remove' },
)

const delegatedProps = reactiveOmit(props, 'class', 'removeLabel')
const item = injectTagsInputItemContext()
const labelId = useId()

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <TagsInputItemDelete
    v-bind="forwardedProps"
    :aria-labelledby="`${labelId} ${item.textId}`"
    :class="['fui-tags-input-item-delete relative bg-transparent outline-none after:absolute after:-inset-1.75', props.class]"
  >
    <span :id="labelId" class="sr-only">{{ removeLabel }}</span>
    <slot>
      <XIcon data-icon />
    </slot>
  </TagsInputItemDelete>
</template>
```

`components/ui/tags-input/TagsInputItemText.vue`

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

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

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

const delegatedProps = reactiveOmit(props, 'class')

const forwardedProps = useForwardProps(delegatedProps)
</script>

<template>
  <TagsInputItemText v-bind="forwardedProps" :class="['fui-tags-input-item-text bg-transparent', props.class]" />
</template>
```
