AvesraAvesrabeta

TagGroup

A focusable list of tags with support for keyboard navigation, selection, and removal.

Import

import { AvTagGroupImports } from '@avesra/angular';

Usage

News
Travel
Gaming
Shopping
import { Component } from '@angular/core';
import { AvTagGroupImports } from '@avesra/angular';

@Component({
  selector: 'app-tag-group-basic-demo',
  imports: [AvTagGroupImports],
  template: `<av-tag-group selection-mode="single" aria-label="Tags">
      <div av-tag-group-list>
        <div av-tag value="news">News</div>
        <div av-tag value="travel">Travel</div>
        <div av-tag value="gaming">Gaming</div>
        <div av-tag value="shopping">Shopping</div>
      </div>
    </av-tag-group>`,
})
export class TagGroupBasicDemo {}

Anatomy

Compose av-tag-group with av-tag-group-list and av-tag items. Use shared av-label / av-description / av-error-message for field chrome. Optional button[av-tag-remove-button] customizes the remove control when allows-removing is set.

<av-tag-group>
  <label av-label></label>
  <div av-tag-group-list>
    <div av-tag value="news">
      News
      <button av-tag-remove-button type="button"></button>
    </div>
  </div>
  <p av-description></p>
  <p av-error-message></p>
</av-tag-group>

Sizes

News
Travel
Gaming
News
Travel
Gaming
News
Travel
Gaming
import { Component } from '@angular/core';
import {
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-sizes-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
  ],
  template: `<div class="flex flex-col gap-6">
      <av-tag-group selection-mode="single" size="sm">
        <label av-label>Small</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
        </div>
      </av-tag-group>
      <av-tag-group selection-mode="single" size="md">
        <label av-label>Medium</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
        </div>
      </av-tag-group>
      <av-tag-group selection-mode="single" size="lg">
        <label av-label>Large</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
        </div>
      </av-tag-group>
    </div>`,
})
export class TagGroupSizesDemo {}

Variants

News
Travel
Gaming
News
Travel
Gaming
import { Component } from '@angular/core';
import {
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-variants-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
  ],
  template: `<div class="flex flex-col gap-8">
      <av-tag-group selection-mode="single" variant="default">
        <label av-label>Default</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
        </div>
      </av-tag-group>
      <av-tag-group selection-mode="single" variant="surface">
        <label av-label>Surface</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
        </div>
      </av-tag-group>
    </div>`,
})
export class TagGroupVariantsDemo {}

Disabled

News
Travel
Gaming

Some tags are disabled

News
Travel
Gaming

Tags disabled via disabled-keys prop

import { Component } from '@angular/core';
import {
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-disabled-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
  ],
  template: `
<div class="flex flex-col gap-4">
  <av-tag-group selection-mode="single">
    <label av-label>Disabled Tags</label>
    <div av-tag-group-list>
      <div av-tag value="news" disabled>News</div>
      <div av-tag value="travel">Travel</div>
      <div av-tag value="gaming" disabled>Gaming</div>
    </div>
    <p av-description>Some tags are disabled</p>
  </av-tag-group>

  <av-tag-group selection-mode="single" [disabled-keys]="['travel']">
    <label av-label>Disabled Keys</label>
    <div av-tag-group-list>
      <div av-tag value="news">News</div>
      <div av-tag value="travel">Travel</div>
      <div av-tag value="gaming">Gaming</div>
    </div>
    <p av-description>Tags disabled via disabled-keys prop</p>
  </av-tag-group>
</div>
`,
})
export class TagGroupDisabledDemo {}

Selection Modes

News
Travel
Gaming
Shopping

Choose one category

News
Travel
Gaming
Shopping

Choose multiple categories

import { Component, signal } from '@angular/core';
import {
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-selection-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
  ],
  template: `<div class="flex flex-col gap-8">
      <av-tag-group
        selection-mode="single"
        [default-selected-keys]="['news']"
        [(selectedKeys)]="singleSelected"
      >
        <label av-label>Single Selection</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
          <div av-tag value="shopping">Shopping</div>
        </div>
        <p av-description>Choose one category</p>
      </av-tag-group>

      <av-tag-group
        selection-mode="multiple"
        [default-selected-keys]="['news', 'travel']"
        [(selectedKeys)]="multipleSelected"
      >
        <label av-label>Multiple Selection</label>
        <div av-tag-group-list>
          <div av-tag value="news">News</div>
          <div av-tag value="travel">Travel</div>
          <div av-tag value="gaming">Gaming</div>
          <div av-tag value="shopping">Shopping</div>
        </div>
        <p av-description>Choose multiple categories</p>
      </av-tag-group>
    </div>`,
})
export class TagGroupSelectionDemo {
  readonly singleSelected = signal<string[]>(['news']);
  readonly multipleSelected = signal<string[]>(['news', 'travel']);
}

Controlled

News
Travel
Gaming
Shopping

Selected: news, travel

import { Component, signal } from '@angular/core';
import {
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-controlled-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
  ],
  template: `
<av-tag-group
  selection-mode="multiple"
  [(selectedKeys)]="selected"
>
  <label av-label>Categories (controlled)</label>
  <div av-tag-group-list>
    <div av-tag value="news">News</div>
    <div av-tag value="travel">Travel</div>
    <div av-tag value="gaming">Gaming</div>
    <div av-tag value="shopping">Shopping</div>
  </div>
  <p av-description>
    Selected: {{ selected().length > 0 ? selected().join(', ') : 'None' }}
  </p>
</av-tag-group>
`,
})
export class TagGroupControlledDemo {
  readonly selected = signal<string[]>(['news', 'travel']);
}

With Error Message

Laundry
Fitness center
Parking
Swimming pool
Breakfast

Select at least one category

import { Component, computed, signal } from '@angular/core';
import {
  AvDescriptionComponent,
  AvErrorMessageComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

@Component({
  selector: 'app-tag-group-with-error-message-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvErrorMessageComponent,
  ],
  template: `
<av-tag-group selection-mode="multiple" [(selectedKeys)]="selected">
  <label av-label>Amenities</label>
  <div av-tag-group-list>
    <div av-tag value="laundry">Laundry</div>
    <div av-tag value="fitness">Fitness center</div>
    <div av-tag value="parking">Parking</div>
    <div av-tag value="pool">Swimming pool</div>
    <div av-tag value="breakfast">Breakfast</div>
  </div>
  <p av-description>
    {{
      isInvalid()
        ? 'Select at least one category'
        : 'Selected: ' + selected().join(', ')
    }}
  </p>
  @if (isInvalid()) {
    <p av-error-message>Please select at least one category</p>
  }
</av-tag-group>
`,
})
export class TagGroupWithErrorMessageDemo {
  readonly selected = signal<string[]>([]);
  readonly isInvalid = computed(() => this.selected().length === 0);
}

With List Data

FredF Fred
MichaelM Michael
JaneJ Jane
AliceA Alice
BobB Bob
CharlieC Charlie

Select team members for your project

Selected:

FredFFred
MichaelMMichael
import { Component, computed, signal } from '@angular/core';
import {
  AvAvatarImports,
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';

type User = {
  id: string;
  name: string;
  avatar: string;
  fallback: string;
};

@Component({
  selector: 'app-tag-group-with-list-data-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvAvatarImports,
  ],
  template: `
<av-tag-group
  selection-mode="multiple"
  allows-removing
  [(selectedKeys)]="selected"
  (remove)="onRemove($event)"
>
  <label av-label>Team Members</label>
  <div av-tag-group-list>
    @for (user of users(); track user.id) {
      <div av-tag [value]="user.id" [text-value]="user.name">
        <span class="size-4" av-avatar>
          <img av-avatar-image [src]="user.avatar" [alt]="user.name" />
          <span av-avatar-fallback>{{ user.fallback }}</span>
        </span>
        {{ user.name }}
      </div>
    } @empty {
      <p class="p-1 text-sm text-muted">No team members</p>
    }
  </div>
  <p av-description>Select team members for your project</p>
</av-tag-group>

@if (selectedUsers().length > 0) {
  <div class="mt-4 flex flex-col gap-2">
    <p class="text-sm font-medium text-muted">Selected:</p>
    <div class="flex flex-wrap gap-2">
      @for (user of selectedUsers(); track user.id) {
        <div class="flex items-center gap-2 rounded-lg bg-surface-tertiary px-2 py-1">
          <span class="size-4" av-avatar>
            <img av-avatar-image [src]="user.avatar" [alt]="user.name" />
            <span av-avatar-fallback>{{ user.fallback }}</span>
          </span>
          <span class="text-sm">{{ user.name }}</span>
        </div>
      }
    </div>
  </div>
}
`,
})
export class TagGroupWithListDataDemo {
  readonly users = signal<User[]>([/* … */]);
  readonly selected = signal<string[]>(['fred', 'michael']);
  readonly selectedUsers = computed(() =>
    this.users().filter((user) => this.selected().includes(user.id)),
  );

  onRemove(keys: string[]): void {
    this.users.update((items) => items.filter((user) => !keys.includes(user.id)));
    this.selected.update((keysSelected) => keysSelected.filter((key) => !keys.includes(key)));
  }
}

With Prefix

News
Travel
Gaming
Shopping

Tags with icons

FredF Fred
MichaelM Michael
JaneJ Jane

Tags with avatars

import { Component } from '@angular/core';
import {
  AvAvatarImports,
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-tag-group-with-prefix-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AvAvatarImports,
    AppIconComponent,
  ],
  template: `
<div class="flex flex-col gap-8">
  <av-tag-group selection-mode="single">
    <label av-label>With Icons</label>
    <div av-tag-group-list>
      <div av-tag value="news">
        <app-icon icon="solar:document-text-linear" size="12" />
        News
      </div>
      <div av-tag value="travel">
        <app-icon icon="solar:global-linear" size="12" />
        Travel
      </div>
      <div av-tag value="gaming">
        <app-icon icon="solar:joystick-linear" size="12" />
        Gaming
      </div>
      <div av-tag value="shopping">
        <app-icon icon="solar:bag-linear" size="12" />
        Shopping
      </div>
    </div>
    <p av-description>Tags with icons</p>
  </av-tag-group>

  <av-tag-group selection-mode="single">
    <label av-label>With Avatars</label>
    <div av-tag-group-list>
      <div av-tag value="fred">
        <span class="size-4" av-avatar>
          <img
            av-avatar-image
            src="/images/gradients/gradient-blue-cyan.png"
            alt="Fred"
          />
          <span av-avatar-fallback>F</span>
        </span>
        Fred
      </div>
      <div av-tag value="michael">
        <span class="size-4" av-avatar>
          <img
            av-avatar-image
            src="/images/gradients/gradient-cyan-blue-purple.png"
            alt="Michael"
          />
          <span av-avatar-fallback>M</span>
        </span>
        Michael
      </div>
      <div av-tag value="jane">
        <span class="size-4" av-avatar>
          <img
            av-avatar-image
            src="/images/gradients/gradient-purple-violet.png"
            alt="Jane"
          />
          <span av-avatar-fallback>J</span>
        </span>
        Jane
      </div>
    </div>
    <p av-description>Tags with avatars</p>
  </av-tag-group>
</div>
`,
})
export class TagGroupWithPrefixDemo {}

With Remove Button

Set allows-removing on the group. When no custom button[av-tag-remove-button] is projected, a default remove button is rendered automatically.

News
Travel
Gaming
Shopping

Click the X to remove tags

React
Vue
Angular
Svelte

Custom remove button using compound composition

import { Component, signal } from '@angular/core';
import {
  AvDescriptionComponent,
  AvLabelComponent,
  AvTagGroupImports,
} from '@avesra/angular';
import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-tag-group-removable-demo',
  imports: [
    AvTagGroupImports,
    AvLabelComponent,
    AvDescriptionComponent,
    AppIconComponent,
  ],
  template: `
<div class="flex flex-col gap-8">
      <av-tag-group selection-mode="single" allows-removing (remove)="onRemove($event)">
        <label av-label>Removable tags</label>
        <div av-tag-group-list>
          @for (tag of tags(); track tag.id) {
            <div av-tag [value]="tag.id" [text-value]="tag.name">{{ tag.name }}</div>
          }
        </div>
        <p av-description>Click the X to remove tags</p>
      </av-tag-group>

      <av-tag-group
        selection-mode="single"
        allows-removing
        (remove)="onRemoveFramework($event)"
      >
        <label av-label>Custom remove button</label>
        <div av-tag-group-list>
          @for (tag of frameworks(); track tag.id) {
            <div av-tag [value]="tag.id" [text-value]="tag.name">
              {{ tag.name }}
              <button av-tag-remove-button type="button" [useDefaultIcon]="false">
                <app-icon icon="solar:close-circle-bold" size="12" />
              </button>
            </div>
          }
        </div>
        <p av-description>Custom remove button using compound composition</p>
      </av-tag-group>
    </div>
`,
})
export class TagGroupRemovableDemo {
  readonly tags = signal([
    { id: 'news', name: 'News' },
    { id: 'travel', name: 'Travel' },
    { id: 'gaming', name: 'Gaming' },
    { id: 'shopping', name: 'Shopping' },
  ]);

  readonly frameworks = signal([
    { id: 'react', name: 'React' },
    { id: 'vue', name: 'Vue' },
    { id: 'angular', name: 'Angular' },
    { id: 'svelte', name: 'Svelte' },
  ]);

  onRemove(keys: string[]): void {
    this.tags.update((items) => items.filter((tag) => !keys.includes(tag.id)));
  }

  onRemoveFramework(keys: string[]): void {
    this.frameworks.update((items) => items.filter((tag) => !keys.includes(tag.id)));
  }
}

Customization

Tailwind CSS

<av-tag-group
  class="**:data-[slot=tag]:rounded-full **:data-[slot=tag]:px-3"
  selection-mode="single"
>
  <div class="gap-2" av-tag-group-list>
    <div av-tag value="news">News</div>
    <div av-tag value="travel">Travel</div>
  </div>
</av-tag-group>

Global CSS

To customize TagGroup classes, use the @layer components directive.

@layer components {
  .av-tag-group {
    @apply flex flex-col gap-2;
  }

  .av-tag-group__list {
    @apply flex flex-wrap gap-2;
  }

  .av-tag {
    @apply rounded-full px-3 py-1;
  }

  .av-tag__remove-button {
    @apply ms-1;
  }
}

Styling Reference

Avesra follows the BEM methodology so variants and states stay reusable and easy to customize.

CSS Classes

Base Classes

  • .av-tag-group — Base tag group container
  • .av-tag-group__list — Container for the list of tags
  • .av-tag — Base tag styles
  • .av-tag__remove-button — Remove button trigger

Slot Classes

  • .av-tag-group [slot="description"] / [data-slot="description"] — Description slot styles
  • .av-tag-group [slot="errorMessage"] / [data-slot="error-message"] — Error message slot styles

Size Classes

  • .av-tag--sm — Small size tag
  • .av-tag--md — Medium size tag (default)
  • .av-tag--lg — Large size tag

Variant Classes

  • .av-tag--default — Default variant
  • .av-tag--surface — Surface variant

State Classes

  • .av-tag[data-selected="true"] — Selected tag state
  • .av-tag[data-disabled="true"] — Disabled tag state
  • .av-tag[data-hovered="true"] — Hovered tag state
  • .av-tag[data-pressed="true"] — Pressed tag state
  • .av-tag[data-focus-visible="true"] — Focused tag state (keyboard focus)

Interactive States

The component supports both CSS pseudo-classes and data attributes:

  • Hover: :hover or [data-hovered="true"] on tag
  • Focus: :focus-visible or [data-focus-visible="true"] on tag
  • Pressed: :active or [data-pressed="true"] on tag
  • Selected: [data-selected="true"] or [aria-selected="true"] on tag
  • Disabled: [data-disabled="true"] or [aria-disabled="true"] on tag

API Reference

TagGroup

Props for av-tag-group. div[av-tag-group-list] has no inputs.

PropTypeDefaultDescription
size'sm' | 'md' | 'lg'—Shared size for child tags.
variant'default' | 'surface'—Shared visual variant for child tags.
disabledbooleanfalseDisables all child tags.
allows-removingbooleanfalseShows remove buttons on child tags and enables remove keyboard shortcuts.
selection-mode'none' | 'single' | 'multiple''none'Whether tags can be selected.
disabled-keysstring[][]Keys that cannot be selected or removed.
default-selected-keysstring[][]Initial selected keys for uncontrolled usage.
selectedKeysstring[]—Selected keys. Supports two-way binding with [(selectedKeys)].
removeOutput<string[]>—Emitted with removed tag keys when a tag is removed.

Tag

Props for div[av-tag] / span[av-tag].

PropTypeDefaultDescription
valuestring—Unique key for the tag (used for selection and remove).
text-valuestring—Accessible text representation when the tag has complex content.
size'sm' | 'md' | 'lg'—Tag size. Inherits from `av-tag-group` when omitted.
variant'default' | 'surface'—Visual variant. Inherits from `av-tag-group` when omitted.
disabledbooleanfalseWhether this tag is disabled.

Tag.RemoveButton

Props for button[av-tag-remove-button]. When allows-removing is set and no custom remove button is projected, a default button is auto-rendered.

PropTypeDefaultDescription
aria-labelstring'Remove tag'Accessible label for the remove action.
disabledbooleanfalseDisables the remove button.
useDefaultIconbooleantrueRenders the built-in close icon when true.

Made with ❤ by SyntaxHertz. Open source and available on GitHub.