Skip to content

Tag List ​

Experimental

A dynamic list of tags shows a remaining counter if all tags cannot be shown in 1 line

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HTagList } from '@holistics/design-system'

const TAGS = [
  'Holistics',
  'Design System',
  'is',
  'simply',
  'the best', '!!!',
] as const

const tags = ref<string[]>(TAGS.slice())

const tagToAdd = ref<string>('')
function addTag () {
  tags.value.push(tagToAdd.value)
  tagToAdd.value = ''
}

function addMany () {
  tags.value.push(...Array.from({ length: 10000 }, (_, i) => (i + 1).toString()))
}

function removeMany () {
  const i = tags.value.findIndex((t) => t === '1')
  tags.value.splice(i, 10000)
}
</script>

<template>
  <div class="w-80">
    <div class="rounded border border-solid px-[7px] py-[5px]">
      <HTagList v-model="tags" />
    </div>

    <div class="mt-4 flex items-center justify-center space-x-4">
      <input
        v-model="tagToAdd"
        type="text"
        class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
        placeholder="Add more tag..."
        @keydown.enter="addTag"
      >

      <HButton
        type="secondary-default"
        label="Refresh"
        icon="refresh"
        @click="tags = TAGS.slice()"
      />
    </div>

    <div class="mt-2 flex items-center justify-center space-x-4">
      <HButton
        type="outline-success"
        label="Add many"
        icon="add"
        class="flex-1"
        @click="addMany"
      />

      <HButton
        type="outline-danger"
        label="Remove many"
        icon="cancel"
        class="flex-1"
        @click="removeMany"
      />
    </div>
  </div>
</template>

Examples ​

Closable ​

vue
<script setup lang="ts">
import { ref } from 'vue'
import { HButton, HTagList } from '@holistics/design-system'

const TAGS = [
  'Holistics',
  'Design System',
  'is',
  'simply',
  'the best', '!!!',
] as const

const tags = ref<string[]>(TAGS.slice())

const tagToAdd = ref<string>('')
function addTag () {
  tags.value.push(tagToAdd.value)
  tagToAdd.value = ''
}

function addManyTags () {
  tags.value.push(...Array.from({ length: 10000 }, (_, i) => (i + 1).toString()))
}

function removeManyTags () {
  const i = tags.value.findIndex((t) => t === '1')
  tags.value.splice(i, 10000)
}
</script>

<template>
  <div class="w-80">
    <div class="rounded border border-solid px-[7px] py-[5px]">
      <HTagList
        v-model="tags"
        closeable
      />
    </div>

    <div class="mt-4 flex items-center justify-center space-x-4">
      <input
        v-model="tagToAdd"
        type="text"
        class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
        placeholder="Add more tag..."
        @keydown.enter="addTag"
      >

      <HButton
        type="secondary-default"
        label="Refresh"
        icon="refresh"
        @click="tags = TAGS.slice()"
      />
    </div>

    <div class="mt-2 flex items-center justify-center space-x-4">
      <HButton
        type="outline-success"
        label="Add many"
        icon="add"
        class="flex-1"
        @click="addManyTags"
      />

      <HButton
        type="outline-danger"
        label="Remove many"
        icon="cancel"
        class="flex-1"
        @click="removeManyTags"
      />
    </div>
  </div>
</template>

Non-remaining Append ​

You can provide an arbitrary slot to render at the append position of the TagList. This will be shown under one of these conditions:

  • When there is no remaining counter tag AND placing it at the append position won't make the TagList overflow.
  • When Popover is shown, it will be placed at the append position in the list.
vue
<script setup lang="ts">
import { HButton, HTagList } from '@holistics/design-system'
import { ref } from 'vue'

const TAGS = [
  'Holistics',
  'Design System',
  'is',
  'simply',
  'the best', '!!!',
] as const

const tags = ref<string[]>(TAGS.slice())

function onClick () {
  alert(`Current tags: [${tags.value.join(', ')}]`)
}

const tagToAdd = ref('')
function addTag () {
  tags.value.push(tagToAdd.value)
  tagToAdd.value = ''
}
</script>

<template>
  <div
    class="space-y-4 text-xs"
    style="width: 448px"
  >
    <div class="flex items-center">
      <div class="w-32">
        Popover <em class="italic">wrapped</em>:
      </div>
      <HTagList
        v-model="tags"
        closeable
        class="ml-1 flex-1"
      >
        <template #non-remaining-append="{ forwardRef }">
          <HButton
            :ref="forwardRef"
            type="clear-default"
            icon="add"
            unified
            @click="onClick"
          />
        </template>
      </HTagList>
    </div>

    <div class="flex items-center">
      <div class="w-32">
        Popover <em class="italic">at counter</em>:
      </div>
      <HTagList
        v-model="tags"
        closeable
        popover="at-counter"
        class="ml-1 flex-1"
      >
        <template #non-remaining-append="{ forwardRef }">
          <HButton
            :ref="forwardRef"
            type="clear-default"
            icon="add"
            unified
            @click="onClick"
          />
        </template>
      </HTagList>
    </div>

    <div class="mt-4 flex items-center justify-center space-x-4">
      <input
        v-model="tagToAdd"
        type="text"
        class="h-8 min-w-0 flex-1 rounded border border-solid border-gray-300 px-2 py-1 text-sm focus:ring-1 focus:ring-blue-200"
        placeholder="Add more tag..."
        @keydown.enter="addTag()"
      >

      <HButton
        type="secondary-default"
        label="Refresh"
        icon="refresh"
        @click="tags = TAGS.slice()"
      />
    </div>
  </div>
</template>

Generics ​

TagList supports generic types by looking at the tags prop.

By default, the component assumes the passed tags is a list of strings. When passing other types, you must either specify the getLabel prop or provide the #tag slot to help the component render the correct labels.

vue
<script setup lang="ts">
import { ref, type Ref } from 'vue'
import {
  HButton,
  HTagList,
  HTag,
  type IconName,
} from '@holistics/design-system'

interface MyTag {
  id: number
  label: string
  icon: IconName
}

const TAGS = [
  { id: 1, label: 'Pinned', icon: 'pin' },
  { id: 2, label: 'Verified', icon: 'check-circle-solid' },
  { id: 3, label: 'Archived', icon: 'archived' },
  { id: 4, label: 'Deleted', icon: 'delete' },
] as const satisfies MyTag[]

const tags = ref(TAGS.slice()) as Ref<MyTag[]>

function getTagTheme (tag: MyTag) {
  switch (tag.id) {
    case 2: return 'green'
    case 3: return 'gray'
    case 4: return 'red'
    default: return undefined
  }
}
</script>

<template>
  <div class="w-80 rounded border border-solid px-[7px] py-[5px]">
    <HTagList
      v-model="tags"
      :get-label="(tag) => tag.label"
    >
      <template #tag="{ tag, index, forwardRef }">
        <HTag
          :ref="forwardRef ? ((ref) => forwardRef(ref, index)) : undefined"
          :tag="tag.label"
          :icon="tag.icon"
          :theme="getTagTheme(tag)"
          closeable
          @close="tags.splice(index, 1)"
        />
      </template>
    </HTagList>
  </div>

  <div class="mt-4 flex justify-center">
    <HButton
      type="secondary-default"
      label="Refresh"
      icon="refresh"
      @click="tags = TAGS.slice()"
    />
  </div>
</template>

API ​

Pass-through ​

  • If popover is wrapped (default): <HPopover>
  • Otherwise: <div>

What does this mean?

All props, events, and attrs that are not specified in the tables below will be passed to the element/component described above.

Props ​

NameTypeDescription
modelValue *
unknown[]
getLabel 
TagListGetLabel<unknown>

A function to get the tag label from the passed generic type T.

gap 
number
= 4

The gap (px) between each tag.

minTagSize 
number
= (p) => { const base = 2 + 16; if (p.closeable) return base + 2 + 16; return base; }

The minimal size (px) of a tag. By default, it's the width of the <Tag> component without any content.

counterTagSize 
TagListCounterTagSizeFn

A function to calculate the size (px) of the remaining-counter tag. Since the counter could be 1 or 1000, the width of the tag is dynamic.

By default, it's the width of the Tag component with the dynamic content +<number>. The default width of the + and all number characters are set to 0.7em which is the maximum width of these characters in the "Inter" font at font-size: 11px.

If you use a different font-family or font-size, you should determine the maximum rendered width of + and all number characters, then update this value accodringly in the calculation.

theme 
"blue" | "green" | "red" | "orange" | "purple" | "gray" | "white" | `#${string}`
closeable 
boolean

Events ​

NameParametersDescription
@close
[event: MouseEvent | KeyboardEvent, tag: unknown, index: number]
@update:modelValue
[tags: unknown[]]

Slots ​

NameScopedDescription
#tag
{ tag: unknown; index: number; visible?: boolean | undefined; forwardRef?: ((element: HTMLElement, index: number) => void) | undefined; }
#counter-tag
{ remainingCount: number; counterTagSize: TagListCounterTagSizeFn; }