AvesraAvesrabeta

Dropdown

A dropdown displays a list of actions or options that a user can choose from a trigger. Built with a floating popover, compound trigger / menu / item / section / indicator parts, and single or multiple selection.

Import

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

Usage

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-basic-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvButtonComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-item id="new-file" textValue="New file">
        <label av-label>New file</label>
      </div>
      <div av-menu-item id="copy-link" textValue="Copy link">
        <label av-label>Copy link</label>
      </div>
      <div av-menu-item id="edit-file" textValue="Edit file">
        <label av-label>Edit file</label>
      </div>
      <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
        <label av-label>Delete file</label>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownBasicDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

Anatomy

Import the Dropdown parts and compose them with attribute selectors.

<av-dropdown>
  <button av-button av-dropdown-trigger></button>
  <av-dropdown-popover>
    <div av-dropdown-menu>
      <div av-menu-item>
        <label av-label></label>
        <p av-description></p>
        <kbd av-kbd></kbd>
        <span av-menu-item-indicator></span>
      </div>
      <div av-separator></div>
      <div av-menu-section>
        <p></p>
        <div av-menu-item></div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>

With Icons

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvKbdImports,
  AvLabelComponent,
} from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-dropdown-with-icons-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvKbdImports, AvButtonComponent, AppIconComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-item id="new-file" textValue="New file">
        <app-icon icon="solar:add-square-linear" size="16" class="shrink-0 text-muted" />
        <label av-label>New file</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>N</span>
        </kbd>
      </div>
      <div av-menu-item id="open-file" textValue="Open file">
        <app-icon icon="solar:folder-open-linear" size="16" class="shrink-0 text-muted" />
        <label av-label>Open file</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>O</span>
        </kbd>
      </div>
      <div av-menu-item id="save-file" textValue="Save file">
        <app-icon icon="solar:diskette-linear" size="16" class="shrink-0 text-muted" />
        <label av-label>Save file</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>S</span>
        </kbd>
      </div>
      <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
        <app-icon icon="solar:trash-bin-trash-linear" size="16" class="shrink-0 text-danger" />
        <label av-label>Delete file</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <abbr av-kbd-abbr key-value="shift"></abbr>
          <span av-kbd-content>D</span>
        </kbd>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownWithIconsDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

With Descriptions

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDescriptionComponent,
  AvDropdownImports,
  AvKbdImports,
  AvLabelComponent,
} from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-dropdown-with-descriptions-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvDescriptionComponent, AvKbdImports, AvButtonComponent, AppIconComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-item id="new-file" textValue="New file">
        <div class="flex h-8 items-start justify-center pt-px">
          <app-icon icon="solar:add-square-linear" size="16" class="shrink-0 text-muted" />
        </div>
        <div class="flex flex-col">
          <label av-label>New file</label>
          <p av-description>Create a new file</p>
        </div>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>N</span>
        </kbd>
      </div>
      <div av-menu-item id="open-file" textValue="Open file">
        <div class="flex h-8 items-start justify-center pt-px">
          <app-icon icon="solar:folder-open-linear" size="16" class="shrink-0 text-muted" />
        </div>
        <div class="flex flex-col">
          <label av-label>Open file</label>
          <p av-description>Open an existing file</p>
        </div>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>O</span>
        </kbd>
      </div>
      <div av-menu-item id="save-file" textValue="Save file">
        <div class="flex h-8 items-start justify-center pt-px">
          <app-icon icon="solar:diskette-linear" size="16" class="shrink-0 text-muted" />
        </div>
        <div class="flex flex-col">
          <label av-label>Save file</label>
          <p av-description>Save the current file</p>
        </div>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>S</span>
        </kbd>
      </div>
      <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
        <div class="flex h-8 items-start justify-center pt-px">
          <app-icon icon="solar:trash-bin-trash-linear" size="16" class="shrink-0 text-danger" />
        </div>
        <div class="flex flex-col">
          <label av-label>Delete file</label>
          <p av-description>Move to trash</p>
        </div>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <abbr av-kbd-abbr key-value="shift"></abbr>
          <span av-kbd-content>D</span>
        </kbd>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownWithDescriptionsDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

With Disabled Items

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDescriptionComponent,
  AvDropdownImports,
  AvKbdImports,
  AvLabelComponent,
  AvSeparatorImports,
} from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-dropdown-with-disabled-items-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvDescriptionComponent, AvKbdImports, AvSeparatorImports, AvButtonComponent, AppIconComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" icon-only aria-label="Menu" av-dropdown-trigger>
    <app-icon icon="solar:hamburger-menu-linear" size="16" />
  </button>
  <av-dropdown-popover>
    <div class="min-w-[220px]">
      <div av-dropdown-menu (action)="onAction($event)">
        <div av-menu-section>
          <p class="px-2.5 py-1 text-xs font-medium text-muted">Actions</p>
          <div av-menu-item id="new-file" textValue="New file">
            <div class="flex h-8 items-start justify-center pt-px">
              <app-icon icon="solar:add-square-linear" size="16" class="shrink-0 text-muted" />
            </div>
            <div class="flex flex-col">
              <label av-label>New file</label>
              <p av-description>Create a new file</p>
            </div>
            <kbd av-kbd variant="light" class="ms-auto">
              <abbr av-kbd-abbr key-value="command"></abbr>
              <span av-kbd-content>N</span>
            </kbd>
          </div>
          <div av-menu-item id="edit-file" textValue="Edit file">
            <div class="flex h-8 items-start justify-center pt-px">
              <app-icon icon="solar:pen-linear" size="16" class="shrink-0 text-muted" />
            </div>
            <div class="flex flex-col">
              <label av-label>Edit file</label>
              <p av-description>Make changes</p>
            </div>
            <kbd av-kbd variant="light" class="ms-auto">
              <abbr av-kbd-abbr key-value="command"></abbr>
              <span av-kbd-content>E</span>
            </kbd>
          </div>
        </div>
        <div av-separator></div>
        <div av-menu-section>
          <p class="px-2.5 py-1 text-xs font-medium text-muted">Danger zone</p>
          <div av-menu-item id="delete-file" textValue="Delete file" variant="danger" disabled>
            <div class="flex h-8 items-start justify-center pt-px">
              <app-icon icon="solar:trash-bin-trash-linear" size="16" class="shrink-0 text-danger" />
            </div>
            <div class="flex flex-col">
              <label av-label>Delete file</label>
              <p av-description>Move to trash</p>
            </div>
            <kbd av-kbd variant="light" class="ms-auto">
              <abbr av-kbd-abbr key-value="command"></abbr>
              <abbr av-kbd-abbr key-value="shift"></abbr>
              <span av-kbd-content>D</span>
            </kbd>
          </div>
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownWithDisabledItemsDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

With Sections

import { Component } from '@angular/core';
import {
  AvButtonComponent,
  AvDescriptionComponent,
  AvDropdownImports,
  AvKbdImports,
  AvLabelComponent,
  AvSeparatorImports,
} from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-dropdown-with-sections-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvDescriptionComponent, AvKbdImports, AvSeparatorImports, AvButtonComponent, AppIconComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" icon-only aria-label="Menu" av-dropdown-trigger>
    <app-icon icon="solar:menu-dots-linear" size="16" />
  </button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-section>
        <p class="px-2.5 py-1 text-xs font-medium text-muted">Actions</p>
        <div av-menu-item id="new-file" textValue="New file">
          <div class="flex h-8 items-start justify-center pt-px">
            <app-icon icon="solar:add-square-linear" size="16" class="shrink-0 text-muted" />
          </div>
          <div class="flex flex-col">
            <label av-label>New file</label>
            <p av-description>Create a new file</p>
          </div>
          <kbd av-kbd variant="light" class="ms-auto">
            <abbr av-kbd-abbr key-value="command"></abbr>
            <span av-kbd-content>N</span>
          </kbd>
        </div>
        <div av-menu-item id="edit-file" textValue="Edit file">
          <div class="flex h-8 items-start justify-center pt-px">
            <app-icon icon="solar:pen-linear" size="16" class="shrink-0 text-muted" />
          </div>
          <div class="flex flex-col">
            <label av-label>Edit file</label>
            <p av-description>Make changes</p>
          </div>
          <kbd av-kbd variant="light" class="ms-auto">
            <abbr av-kbd-abbr key-value="command"></abbr>
            <span av-kbd-content>E</span>
          </kbd>
        </div>
      </div>
      <div av-separator></div>
      <div av-menu-section>
        <p class="px-2.5 py-1 text-xs font-medium text-muted">Danger zone</p>
        <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
          <div class="flex h-8 items-start justify-center pt-px">
            <app-icon icon="solar:trash-bin-trash-linear" size="16" class="shrink-0 text-danger" />
          </div>
          <div class="flex flex-col">
            <label av-label>Delete file</label>
            <p av-description>Move to trash</p>
          </div>
          <kbd av-kbd variant="light" class="ms-auto">
            <abbr av-kbd-abbr key-value="command"></abbr>
            <abbr av-kbd-abbr key-value="shift"></abbr>
            <span av-kbd-content>D</span>
          </kbd>
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownWithSectionsDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

With Multiple Selection

Set selection-mode="multiple" to let users toggle several items without closing the menu. Project a custom av-checkbox into the indicator and bind it with #item="avMenuItem" and [selected]="item.isSelected()".

import { Component, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvCheckboxImports,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-multiple-selection-demo',
  imports: [AvDropdownImports, AvCheckboxImports, AvLabelComponent, AvButtonComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" av-dropdown-trigger>Preferred fruits</button>
  <av-dropdown-popover>
    <div class="min-w-[256px]">
      <div av-dropdown-menu selection-mode="multiple" [(selectedKeys)]="selected">
        <div av-menu-section>
          <p class="px-2.5 py-1 text-xs font-medium text-muted">Select fruits</p>
          <div av-menu-item #apple="avMenuItem" id="multi-apple" textValue="Apple">
            <span av-menu-item-indicator>
              <div av-checkbox class="pointer-events-none" [selected]="apple.isSelected()" aria-hidden="true">
                <span av-checkbox-control>
                  <span av-checkbox-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Apple</label>
          </div>
          <div av-menu-item #banana="avMenuItem" id="multi-banana" textValue="Banana">
            <span av-menu-item-indicator>
              <div av-checkbox class="pointer-events-none" [selected]="banana.isSelected()" aria-hidden="true">
                <span av-checkbox-control>
                  <span av-checkbox-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Banana</label>
          </div>
          <div av-menu-item #cherry="avMenuItem" id="multi-cherry" textValue="Cherry">
            <span av-menu-item-indicator>
              <div av-checkbox class="pointer-events-none" [selected]="cherry.isSelected()" aria-hidden="true">
                <span av-checkbox-control>
                  <span av-checkbox-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Cherry</label>
          </div>
        </div>
        <div av-menu-item #orange="avMenuItem" id="multi-orange" textValue="Orange">
          <span av-menu-item-indicator>
            <div av-checkbox class="pointer-events-none" [selected]="orange.isSelected()" aria-hidden="true">
              <span av-checkbox-control>
                <span av-checkbox-indicator></span>
              </span>
            </div>
          </span>
          <label av-label>Orange</label>
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownMultipleSelectionDemo {
  readonly selected = signal<string[]>(['multi-apple', 'multi-banana']);
}

Controlled

Selected: bold

import { Component, computed, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-controlled-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvButtonComponent],
  template: `<div class="flex min-w-sm flex-col items-center justify-center gap-4">
  <p class="text-sm text-muted">Selected: {{ selectedLabel() }}</p>
  <av-dropdown>
    <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
    <av-dropdown-popover>
      <div av-dropdown-menu selection-mode="multiple" [(selectedKeys)]="selected">
        <div av-menu-item id="bold" textValue="Bold">
          <label av-label>Bold</label>
          <span av-menu-item-indicator></span>
        </div>
        <div av-menu-item id="italic" textValue="Italic">
          <label av-label>Italic</label>
          <span av-menu-item-indicator></span>
        </div>
        <div av-menu-item id="underline" textValue="Underline">
          <label av-label>Underline</label>
          <span av-menu-item-indicator></span>
        </div>
      </div>
    </av-dropdown-popover>
  </av-dropdown>
</div>`,
})
export class DropdownControlledDemo {
  readonly selected = signal<string[]>(['bold']);
  readonly selectedLabel = computed(() => {
    const keys = this.selected();
    return keys.length > 0 ? keys.join(', ') : 'None';
  });
}

Controlled Open State

Dropdown is: closed

import { Component, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-controlled-open-state-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvButtonComponent],
  template: `<div class="flex min-w-sm flex-col items-center justify-center gap-4">
  <p class="text-sm text-muted">
    Dropdown is: <strong>{{ open() ? 'open' : 'closed' }}</strong>
  </p>
  <av-dropdown [(open)]="open">
    <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
    <av-dropdown-popover>
      <div av-dropdown-menu>
        <div av-menu-item id="new-file" textValue="New file">
          <label av-label>New file</label>
        </div>
        <div av-menu-item id="open-file" textValue="Open file">
          <label av-label>Open file</label>
        </div>
        <div av-menu-item id="save-file" textValue="Save file">
          <label av-label>Save file</label>
        </div>
        <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
          <label av-label>Delete file</label>
        </div>
      </div>
    </av-dropdown-popover>
  </av-dropdown>
</div>`,
})
export class DropdownControlledOpenStateDemo {
  readonly open = signal(false);
}

With Single Selection

import { Component, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-single-selection-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvButtonComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Fruit</button>
  <av-dropdown-popover>
    <div class="min-w-[256px]">
      <div av-dropdown-menu selection-mode="single" [(selectedKeys)]="selected">
        <div av-menu-section>
          <p class="px-2.5 py-1 text-xs font-medium text-muted">Select a fruit</p>
          <div av-menu-item id="apple" textValue="Apple">
            <span av-menu-item-indicator></span>
            <label av-label>Apple</label>
          </div>
          <div av-menu-item id="banana" textValue="Banana">
            <span av-menu-item-indicator></span>
            <label av-label>Banana</label>
          </div>
          <div av-menu-item id="cherry" textValue="Cherry">
            <span av-menu-item-indicator></span>
            <label av-label>Cherry</label>
          </div>
        </div>
        <div av-menu-item id="orange" textValue="Orange">
          <span av-menu-item-indicator></span>
          <label av-label>Orange</label>
        </div>
        <div av-menu-item id="pear" textValue="Pear">
          <span av-menu-item-indicator></span>
          <label av-label>Pear</label>
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownSingleSelectionDemo {
  readonly selected = signal<string[]>(['apple']);
}

Single With Custom Indicator

import { Component, signal } from '@angular/core';
import {
  AvButtonComponent,
  AvDropdownImports,
  AvLabelComponent,
  AvRadioImports,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-single-custom-indicator-demo',
  imports: [AvDropdownImports, AvRadioImports, AvLabelComponent, AvButtonComponent],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Fruits</button>
  <av-dropdown-popover>
    <div class="min-w-[256px]">
      <div av-dropdown-menu selection-mode="single" [(selectedKeys)]="selected">
        <div av-menu-section>
          <p class="px-2.5 py-1 text-xs font-medium text-muted">Select a fruit</p>
          <div av-menu-item #apple="avMenuItem" id="apple" textValue="Apple">
            <span av-menu-item-indicator>
              <div av-radio class="pointer-events-none" [selected]="apple.isSelected()" aria-hidden="true">
                <span av-radio-control>
                  <span av-radio-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Apple</label>
          </div>
          <div av-menu-item #banana="avMenuItem" id="banana" textValue="Banana">
            <span av-menu-item-indicator>
              <div av-radio class="pointer-events-none" [selected]="banana.isSelected()" aria-hidden="true">
                <span av-radio-control>
                  <span av-radio-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Banana</label>
          </div>
          <div av-menu-item #cherry="avMenuItem" id="cherry" textValue="Cherry">
            <span av-menu-item-indicator>
              <div av-radio class="pointer-events-none" [selected]="cherry.isSelected()" aria-hidden="true">
                <span av-radio-control>
                  <span av-radio-indicator></span>
                </span>
              </div>
            </span>
            <label av-label>Cherry</label>
          </div>
        </div>
        <div av-menu-item #orange="avMenuItem" id="orange" textValue="Orange">
          <span av-menu-item-indicator>
            <div av-radio class="pointer-events-none" [selected]="orange.isSelected()" aria-hidden="true">
              <span av-radio-control>
                <span av-radio-indicator></span>
              </span>
            </div>
          </span>
          <label av-label>Orange</label>
        </div>
        <div av-menu-item #pear="avMenuItem" id="pear" textValue="Pear">
          <span av-menu-item-indicator>
            <div av-radio class="pointer-events-none" [selected]="pear.isSelected()" aria-hidden="true">
              <span av-radio-control>
                <span av-radio-indicator></span>
              </span>
            </div>
          </span>
          <label av-label>Pear</label>
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownSingleCustomIndicatorDemo {
  readonly selected = signal<string[]>(['apple']);
}

With Keyboard Shortcuts

Compose kbd[av-kbd] inside av-menu-item with class="ms-auto" to show trailing keyboard shortcuts.

import { Component } from '@angular/core';

import {
  AvButtonComponent,
  AvDropdownImports,
  AvKbdImports,
  AvLabelComponent,
} from '@avesra/angular';

@Component({
  selector: 'app-dropdown-with-keyboard-shortcuts-demo',
  imports: [
    AvDropdownImports,
    AvLabelComponent,
    AvKbdImports,
    AvButtonComponent,
  ],
  template: `<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>Actions</button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-item id="new" textValue="New">
        <label av-label>New</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>N</span>
        </kbd>
      </div>
      <div av-menu-item id="open" textValue="Open">
        <label av-label>Open</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>O</span>
        </kbd>
      </div>
      <div av-menu-item id="save" textValue="Save">
        <label av-label>Save</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <span av-kbd-content>S</span>
        </kbd>
      </div>
      <div av-menu-item id="delete" textValue="Delete" variant="danger">
        <label av-label>Delete</label>
        <kbd av-kbd variant="light" class="ms-auto">
          <abbr av-kbd-abbr key-value="command"></abbr>
          <abbr av-kbd-abbr key-value="shift"></abbr>
          <span av-kbd-content>D</span>
        </kbd>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownWithKeyboardShortcutsDemo {
  onAction(key: string): void {
    console.log(`Selected: ${key}`);
  }
}

Custom Trigger

import { Component } from '@angular/core';
import {
  AvAvatarImports,
  AvDropdownImports,
  AvLabelComponent,
} from '@avesra/angular';

import { AppIconComponent } from '../../components/app-icon/app-icon.component';

@Component({
  selector: 'app-dropdown-custom-trigger-demo',
  imports: [AvDropdownImports, AvLabelComponent, AvAvatarImports, AppIconComponent],
  template: `<av-dropdown>
  <button
    av-dropdown-trigger
    custom-trigger
    type="button"
    class="rounded-full outline-none"
    aria-label="User menu"
  >
    <span av-avatar>
      <img av-avatar-image alt="Junior Garcia" src="/images/gradients/gradient-warm-orange-yellow-red.png" />
      <span av-avatar-fallback delay-ms="600">JD</span>
    </span>
  </button>
  <av-dropdown-popover>
    <div class="px-3 pt-3 pb-1">
      <div class="flex items-center gap-2">
        <span av-avatar size="sm">
          <img av-avatar-image alt="Jane" src="/images/gradients/gradient-warm-orange-yellow-red.png" />
          <span av-avatar-fallback delay-ms="600">JD</span>
        </span>
        <div class="flex flex-col gap-0">
          <p class="text-sm leading-5 font-medium">Jane Doe</p>
          <p class="text-xs leading-none text-muted">jane&#64;example.com</p>
        </div>
      </div>
    </div>
    <div av-dropdown-menu>
      <div av-menu-item id="dashboard" textValue="Dashboard">
        <label av-label>Dashboard</label>
      </div>
      <div av-menu-item id="profile" textValue="Profile">
        <label av-label>Profile</label>
      </div>
      <div av-menu-item id="settings" textValue="Settings">
        <div class="flex w-full items-center justify-between gap-2">
          <label av-label>Settings</label>
          <app-icon icon="solar:settings-linear" size="14" class="text-muted" />
        </div>
      </div>
      <div av-menu-item id="new-project" textValue="New project">
        <div class="flex w-full items-center justify-between gap-2">
          <label av-label>Create Team</label>
          <app-icon icon="solar:users-group-rounded-linear" size="14" class="text-muted" />
        </div>
      </div>
      <div av-menu-item id="logout" textValue="Logout" variant="danger">
        <div class="flex w-full items-center justify-between gap-2">
          <label av-label>Log Out</label>
          <app-icon icon="solar:logout-2-linear" size="14" class="text-danger" />
        </div>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>`,
})
export class DropdownCustomTriggerDemo {}

Styling

Pass utility classes on the host elements, or override BEM classes in @layer components.

CSS Classes

  • .av-dropdown — Base dropdown container
  • .av-dropdown__trigger — The element that toggles the popover
  • .av-dropdown__popover — The floating popover panel
  • .av-dropdown__menu — The menu container inside the popover

Dropdown composes the shared av-menu-item and av-menu-section primitives, so their classes are also available for customization:

  • .av-menu-item — Individual menu item container
  • .av-menu-item--danger — Danger item variant
  • .av-menu-item__indicator — Selection indicator (checkmark or dot)
  • [data-slot="menu-item-indicator--checkmark"] — Checkmark indicator SVG
  • [data-slot="menu-item-indicator--dot"] — Dot indicator SVG
  • .av-menu-section — Section group (role="group")
  • [data-slot="separator"] — Separator elements within the menu

Interactive States

Trigger and item styles support both CSS pseudo-classes and data attributes:

  • Hover::hover or [data-hovered="true"] on items
  • Pressed::active or [data-pressed="true"]
  • Focus::focus-visible or [data-focus-visible="true"]
  • Selected:[data-selected="true"] or [aria-checked="true"]
  • Disabled:[data-disabled="true"] or [aria-disabled="true"]
  • Entering/exiting popover:[data-entering="true"] / [data-exiting="true"]
  • Selection mode:.av-dropdown__menu[data-selection-mode="single"] / [data-selection-mode="multiple"]

Examples

Basic Usage

<av-dropdown>
  <button av-button variant="secondary" aria-label="Menu" av-dropdown-trigger>
    Actions
  </button>
  <av-dropdown-popover>
    <div av-dropdown-menu (action)="onAction($event)">
      <div av-menu-item id="new-file" textValue="New file">
        <label av-label>New file</label>
      </div>
      <div av-menu-item id="open-file" textValue="Open file">
        <label av-label>Open file</label>
      </div>
      <div av-menu-item id="delete-file" textValue="Delete file" variant="danger">
        <label av-label>Delete file</label>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>

Controlled Selection

Drive selection from a signal<string[]> and bind it with [(selectedKeys)] for two-way sync between the menu and outside state.

<av-dropdown>
  <button av-button variant="secondary" aria-label="Format" av-dropdown-trigger>
    Format
  </button>
  <av-dropdown-popover>
    <div
      av-dropdown-menu
      selection-mode="multiple"
      [(selectedKeys)]="selected"
    >
      <div av-menu-item id="bold" textValue="Bold">
        <label av-label>Bold</label>
        <span av-menu-item-indicator></span>
      </div>
      <div av-menu-item id="italic" textValue="Italic">
        <label av-label>Italic</label>
        <span av-menu-item-indicator></span>
      </div>
    </div>
  </av-dropdown-popover>
</av-dropdown>

Accessibility

Dropdown implements the ARIA menu pattern via Angular CDK Menu and provides:

  • Full keyboard navigation (arrow keys, Enter/Space, Escape to dismiss)
  • Typeahead search using each item's textValue
  • aria-haspopup="menu" and aria-expanded automatically managed on the trigger
  • Focus is moved to the first item on open and returned to the trigger on close
  • Proper aria-checked / aria-disabled item states for selection
  • Section containers expose role="group"; indicators are aria-hidden

Always provide an accessible label for the trigger — either visible text or aria-label when using an icon-only button.

API

Compound API across av-dropdown, av-dropdown-trigger, av-dropdown-popover, av-dropdown-menu, av-menu-item, av-menu-item-indicator, and av-menu-section (no inputs). See also List Box, which shares the same item and section composition for non-overlay selection lists.

PropTypeDefaultDescription
openbooleanfalseControls whether the dropdown is open. Supports two-way binding with [(open)] (av-dropdown).
dismissablebooleantrueCloses the dropdown when clicking outside (av-dropdown).
keyboard-dismiss-disabledbooleanfalseDisables closing via the Escape key (av-dropdown).
disabledbooleanfalseDisables the trigger (av-dropdown-trigger).
custom-triggerbooleanfalseApplies trigger styles to non-button elements (av-dropdown-trigger).
placement'top' | 'top start' | 'top end' | 'bottom' | 'bottom start' | 'bottom end' | 'left' | 'left top' | 'left bottom' | 'right' | 'right top' | 'right bottom' | 'start' | 'start top' | 'start bottom' | 'end' | 'end top' | 'end bottom''bottom'Preferred placement relative to the trigger (av-dropdown-popover).
offsetnumber8Distance between trigger and menu in pixels (av-dropdown-popover).
classstring—Extra classes applied to the overlay panel (av-dropdown-popover).
should-flipbooleantrueWhether the menu can flip to fit the viewport (av-dropdown-popover).
selection-mode'none' | 'single' | 'multiple''none'How menu items can be selected (av-dropdown-menu).
selectedKeysstring[][]Selected item ids. Supports two-way binding with [(selectedKeys)] (av-dropdown-menu).
disabledbooleanfalseDisables all menu items (av-dropdown-menu).
actionEventEmitter<string>—Emits the item id when activated (av-dropdown-menu).
idstring—Required unique item id (av-menu-item).
textValuestring—Accessible label used for typeahead search (av-menu-item).
variant'default' | 'danger''default'Visual style variant for the item (av-menu-item).
disabledbooleanfalseDisables a single item (av-menu-item).
classstring—Extra classes merged onto the menu item host (av-menu-item).
type'checkmark' | 'dot''checkmark'Selection indicator style (av-menu-item-indicator).

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