Select

Displays a list of options for the user to pick from—triggered by a button.

PreviousNext
Selected value:
import { ChangeDetectionStrategy, Component } from '@angular/core';
 
import { ZardBadgeComponent } from '../../badge/badge.component';
import { ZardSelectItemComponent } from '../select-item.component';
import { ZardSelectComponent } from '../select.component';
 
@Component({
  selector: 'z-demo-select-basic',
  imports: [ZardBadgeComponent, ZardSelectComponent, ZardSelectItemComponent],
  template: `
    <div class="flex flex-col gap-4">
      <span>
        Selected value:
        @if (selectedValue) {
          <z-badge>{{ selectedValue }}</z-badge>
        }
      </span>
      <z-select class="w-[300px]" zPlaceholder="Select a fruit" [(zValue)]="selectedValue">
        <z-select-item zValue="apple">Apple</z-select-item>
        <z-select-item zValue="banana">Banana</z-select-item>
        <z-select-item zValue="blueberry">Blueberry</z-select-item>
        <z-select-item zValue="grapes">Grapes</z-select-item>
        <z-select-item zValue="pineapple" zDisabled>Pineapple</z-select-item>
      </z-select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoSelectBasicComponent {
  selectedValue = '';
}
 

Installation

1

Run the CLI

Use the CLI to add the component to your project.

npx @ngzard/ui@latest add select
1

Add the component files

Create the component directory structure and add the following files to your project.

select.component.ts
select.component.ts
import { Overlay, OverlayModule, OverlayPositionBuilder, type OverlayRef } from '@angular/cdk/overlay';
import { TemplatePortal } from '@angular/cdk/portal';
import { CommonModule, isPlatformBrowser } from '@angular/common';
import {
  type AfterContentInit,
  afterNextRender,
  ChangeDetectionStrategy,
  Component,
  computed,
  contentChildren,
  DestroyRef,
  ElementRef,
  forwardRef,
  inject,
  Injector,
  input,
  model,
  type OnDestroy,
  output,
  PLATFORM_ID,
  runInInjectionContext,
  signal,
  type TemplateRef,
  viewChild,
  ViewContainerRef,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { type ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
 
import type { ClassValue } from 'clsx';
import { filter } from 'rxjs';
 
import { ZardSelectItemComponent } from './select-item.component';
import {
  selectContentVariants,
  selectTriggerVariants,
  selectVariants,
  type ZardSelectSizeVariants,
} from './select.variants';
import { mergeClasses, transform } from '../../shared/utils/utils';
import { ZardBadgeComponent } from '../badge/badge.component';
import { checkForProperZardInitialization } from '../core/provider/providezard';
import { ZardIconComponent } from '../icon/icon.component';
 
type OnTouchedType = () => void;
type OnChangeType = (value: string) => void;
 
const COMPACT_MODE_WIDTH_THRESHOLD = 100;
 
@Component({
  selector: 'z-select, [z-select]',
  imports: [CommonModule, OverlayModule, ZardBadgeComponent, ZardIconComponent],
  template: `
    <button
      type="button"
      [class]="triggerClasses()"
      [disabled]="zDisabled()"
      (click)="toggle()"
      (keydown.{enter,space,arrowdown,arrowup,escape}.prevent)="onTriggerKeydown($event)"
      [attr.aria-expanded]="isOpen()"
      [attr.aria-haspopup]="'listbox'"
      [attr.data-state]="isOpen() ? 'open' : 'closed'"
      [attr.data-placeholder]="!zValue() ? '' : null"
      [tabIndex]="0"
      (focus)="onFocus()"
      (blur)="!isOpen() && isFocus.set(false)"
    >
      <span class="flex flex-1 flex-wrap items-center gap-2">
        @let labels = selectedLabels();
        @for (label of labels; track index; let index = $index) {
          <ng-container *ngTemplateOutlet="labelsTemplate; context: { $implicit: label }" />
        } @empty {
          <span class="text-muted-foreground truncate">{{ zPlaceholder() }}</span>
        }
      </span>
      <z-icon zType="chevron-down" zSize="lg" class="opacity-50" />
    </button>
 
    <ng-template #labelsTemplate let-label>
      @if (zMultiple()) {
        <z-badge zType="secondary">
          <span class="truncate">{{ label }}</span>
        </z-badge>
      } @else {
        <span class="truncate">{{ label }}</span>
      }
    </ng-template>
 
    <ng-template #dropdownTemplate>
      <div
        [class]="contentClasses()"
        role="listbox"
        [attr.data-state]="'open'"
        (keydown.{arrowdown,arrowup,enter,space,escape,home,end}.prevent)="onDropdownKeydown($event)"
        tabindex="-1"
      >
        <div class="p-1">
          <ng-content />
        </div>
      </div>
    </ng-template>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => ZardSelectComponent),
      multi: true,
    },
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    '[attr.data-active]': 'isFocus() ? "" : null',
    '[attr.data-disabled]': 'zDisabled() ? "" : null',
    '[attr.data-state]': 'isOpen() ? "open" : "closed"',
    '[class]': 'classes()',
  },
})
export class ZardSelectComponent implements ControlValueAccessor, AfterContentInit, OnDestroy {
  private readonly destroyRef = inject(DestroyRef);
  private readonly elementRef = inject(ElementRef<HTMLElement>);
  private readonly injector = inject(Injector);
  private readonly overlay = inject(Overlay);
  private readonly overlayPositionBuilder = inject(OverlayPositionBuilder);
  private readonly viewContainerRef = inject(ViewContainerRef);
  private readonly platformId = inject(PLATFORM_ID);
 
  readonly dropdownTemplate = viewChild.required<TemplateRef<void>>('dropdownTemplate');
  readonly selectItems = contentChildren(ZardSelectItemComponent);
 
  private overlayRef?: OverlayRef;
  private portal?: TemplatePortal;
 
  readonly class = input<ClassValue>('');
  readonly zDisabled = input(false, { transform });
  readonly zLabel = input<string>('');
  readonly zMaxLabelCount = input<number>(1);
  readonly zMultiple = input<boolean>(false);
  readonly zPlaceholder = input<string>('Select an option...');
  readonly zSize = input<ZardSelectSizeVariants>('default');
  readonly zValue = model<string | string[]>(this.zMultiple() ? [] : '');
 
  readonly zSelectionChange = output<string | string[]>();
 
  readonly isOpen = signal(false);
  readonly focusedIndex = signal<number>(-1);
  protected readonly isFocus = signal(false);
  protected readonly isCompact = signal(false);
 
  protected onFocus(): void {
    if (this.isCompact()) {
      this.isFocus.set(true);
    }
  }
 
  // Compute the label based on selected value
  readonly selectedLabels = computed<string[]>(() => {
    const selectedValue = this.zValue();
    if (this.zMultiple() && Array.isArray(selectedValue)) {
      return this.provideLabelsForMultiselectMode(selectedValue);
    }
 
    return this.provideLabelForSingleSelectMode(selectedValue as string);
  });
 
  constructor() {
    checkForProperZardInitialization();
  }
 
  private onChange: OnChangeType = (_value: string) => {
    // ControlValueAccessor onChange callback
  };
 
  private onTouched: OnTouchedType = () => {
    // ControlValueAccessor onTouched callback
  };
 
  protected readonly classes = computed(() => mergeClasses(selectVariants(), this.class()));
  protected readonly contentClasses = computed(() => mergeClasses(selectContentVariants()));
  protected readonly triggerClasses = computed(() =>
    mergeClasses(
      selectTriggerVariants({
        zSize: this.zSize(),
      }),
    ),
  );
 
  ngAfterContentInit() {
    const hostWidth = this.elementRef.nativeElement.offsetWidth || 0;
    // Setup select host reference for each item
    for (const item of this.selectItems()) {
      item.setSelectHost({
        selectedValue: () => (this.zMultiple() ? (this.zValue() as string[]) : [this.zValue() as string]),
        selectItem: (value: string, label: string) => this.selectItem(value, label),
      });
      item.zSize.set(this.zSize());
 
      if (hostWidth <= COMPACT_MODE_WIDTH_THRESHOLD) {
        this.isCompact.set(true);
        item.zMode.set('compact');
      }
    }
  }
 
  ngOnDestroy() {
    this.destroyOverlay();
  }
 
  onTriggerKeydown(event: Event) {
    const { key } = event as KeyboardEvent;
    switch (key) {
      case 'Enter':
      case ' ':
      case 'ArrowDown':
      case 'ArrowUp':
        if (!this.isOpen()) {
          this.open();
        }
        break;
      case 'Escape':
        if (this.isOpen()) {
          this.close();
        }
        break;
    }
  }
 
  onDropdownKeydown(e: Event) {
    const { key } = e as KeyboardEvent;
    const items = this.getSelectItems();
 
    switch (key) {
      case 'ArrowDown':
        this.navigateItems(1, items);
        break;
      case 'ArrowUp':
        this.navigateItems(-1, items);
        break;
      case 'Enter':
      case ' ':
        this.selectFocusedItem(items);
        break;
      case 'Escape':
        this.close();
        this.focusButton();
        break;
      case 'Home':
        this.focusFirstItem(items);
        break;
      case 'End':
        this.focusLastItem(items);
        break;
    }
  }
 
  toggle() {
    if (this.zDisabled()) {
      return;
    }
 
    if (this.isOpen()) {
      this.close();
    } else {
      this.open();
    }
  }
 
  selectItem(value: string, label: string) {
    if (value === undefined || value === null || value === '') {
      console.warn('Attempted to select item with invalid value:', { value, label });
      return;
    }
 
    this.zValue.update(selectedValues => {
      if (Array.isArray(selectedValues)) {
        return selectedValues.includes(value) ? selectedValues.filter(v => v !== value) : [...selectedValues, value];
      }
 
      return value;
    });
    this.onChange(value);
    this.zSelectionChange.emit(this.zValue());
 
    if (this.zMultiple()) {
      // in multiple mode it can happen that button changes size because of selection badges,
      // which requires overlay position to update
      this.updateOverlayPosition();
    } else {
      this.close();
 
      // Return focus to the button after selection
      setTimeout(() => {
        this.focusButton();
      }, 0);
    }
  }
 
  private updateOverlayPosition(): void {
    setTimeout(() => {
      this.overlayRef?.updatePosition();
    }, 0);
  }
 
  private provideLabelsForMultiselectMode(selectedValue: string[]): string[] {
    const labelsToShowCount = selectedValue.length - this.zMaxLabelCount();
    const labels = [];
    let index = 0;
    for (const value of selectedValue) {
      const matchingItem = this.getMatchingItem(value);
      if (matchingItem) {
        labels.push(matchingItem.label());
        index++;
      }
      if (labelsToShowCount && this.zMaxLabelCount() && index === this.zMaxLabelCount()) {
        labels.push(`${labelsToShowCount} more item${labelsToShowCount > 1 ? 's' : ''} selected`);
        break;
      }
    }
    return labels;
  }
 
  private provideLabelForSingleSelectMode(selectedValue: string): string[] {
    const manualLabel = this.zLabel();
    if (manualLabel) {
      return [manualLabel];
    }
 
    const matchingItem = this.getMatchingItem(selectedValue);
    if (matchingItem) {
      return [matchingItem.label()];
    }
 
    return selectedValue ? [selectedValue] : [];
  }
 
  private open() {
    if (this.isOpen()) {
      return;
    }
 
    // Create overlay if it doesn't exist
    if (!this.overlayRef) {
      this.createOverlay();
    }
 
    if (!this.overlayRef) {
      return;
    }
 
    const hostWidth = this.elementRef.nativeElement.offsetWidth || 0;
 
    if (this.overlayRef.hasAttached()) {
      this.overlayRef.detach();
    }
 
    this.portal = new TemplatePortal(this.dropdownTemplate(), this.viewContainerRef);
 
    this.overlayRef.attach(this.portal);
    this.overlayRef.updateSize({ width: hostWidth });
    this.isOpen.set(true);
 
    this.determinePortalWidthOnOpen(hostWidth);
  }
 
  private setFocusOnOpen(): void {
    this.focusDropdown();
    this.focusSelectedItem();
  }
 
  private close() {
    if (this.overlayRef?.hasAttached()) {
      this.overlayRef.detach();
    }
    this.isOpen.set(false);
    this.focusedIndex.set(-1);
    this.onTouched();
  }
 
  private getMatchingItem(value: string): ZardSelectItemComponent | undefined {
    return this.selectItems()?.find(item => item.zValue() === value);
  }
 
  private determinePortalWidthOnOpen(portalWidth: number): void {
    runInInjectionContext(this.injector, () => {
      afterNextRender(() => {
        if (!this.overlayRef || !this.overlayRef.hasAttached()) {
          return;
        }
 
        const overlayPaneElement = this.overlayRef.overlayElement;
        const textElements = Array.from(
          overlayPaneElement.querySelectorAll<HTMLElement>(
            'z-select-item > span.truncate, [z-select-item] > span.truncate',
          ),
        );
        let isOverflow = false;
        for (const textElement of textElements) {
          if (textElement.scrollWidth > textElement.clientWidth + 1) {
            isOverflow = true;
            break;
          }
        }
 
        if (!isOverflow) {
          this.setFocusOnOpen();
          return;
        }
 
        const selectItems = this.selectItems();
        let itemMaxWidth = 0;
        for (const item of selectItems) {
          itemMaxWidth = Math.max(itemMaxWidth, item.elementRef.nativeElement.scrollWidth);
        }
 
        const [selectItem] = selectItems;
        if (isOverflow && selectItem) {
          const elementStyles = getComputedStyle(selectItem.elementRef.nativeElement);
          const leftPadding = Number.parseFloat(elementStyles.getPropertyValue('padding-left')) || 0;
          const rightPadding = Number.parseFloat(elementStyles.getPropertyValue('padding-right')) || 0;
          itemMaxWidth += leftPadding + rightPadding;
        }
 
        itemMaxWidth = Math.max(itemMaxWidth, portalWidth);
        this.overlayRef.updateSize({ width: itemMaxWidth });
        this.overlayRef.updatePosition();
 
        this.setFocusOnOpen();
      });
    });
  }
 
  private createOverlay() {
    if (this.overlayRef) {
      return;
    } // Already created
 
    if (isPlatformBrowser(this.platformId)) {
      try {
        const positionStrategy = this.overlayPositionBuilder
          .flexibleConnectedTo(this.elementRef)
          .withPositions([
            {
              originX: 'center',
              originY: 'bottom',
              overlayX: 'center',
              overlayY: 'top',
              offsetY: 4,
            },
            {
              originX: 'center',
              originY: 'top',
              overlayX: 'center',
              overlayY: 'bottom',
              offsetY: -4,
            },
          ])
          .withPush(false);
 
        const elementWidth = this.elementRef.nativeElement.offsetWidth || 200;
 
        this.overlayRef = this.overlay.create({
          positionStrategy,
          hasBackdrop: false,
          scrollStrategy: this.overlay.scrollStrategies.reposition(),
          width: elementWidth,
          maxHeight: 384, // max-h-96 equivalent
        });
        this.overlayRef
          .outsidePointerEvents()
          .pipe(
            filter(event => !this.elementRef.nativeElement.contains(event.target)),
            takeUntilDestroyed(this.destroyRef),
          )
          .subscribe(() => {
            this.isFocus.set(false);
            this.close();
          });
      } catch (error) {
        console.error('Error creating overlay:', error);
      }
    }
  }
 
  private destroyOverlay() {
    if (this.overlayRef) {
      this.overlayRef.dispose();
      this.overlayRef = undefined;
    }
  }
 
  private getSelectItems(): HTMLElement[] {
    if (!this.overlayRef?.hasAttached()) {
      return [];
    }
    const dropdownElement = this.overlayRef.overlayElement;
    return Array.from(dropdownElement.querySelectorAll<HTMLElement>('z-select-item, [z-select-item]')).filter(
      item => item.dataset['disabled'] === undefined,
    );
  }
 
  private navigateItems(direction: number, items: HTMLElement[]) {
    if (items.length === 0) {
      return;
    }
 
    const currentIndex = this.focusedIndex();
    let nextIndex = currentIndex + direction;
 
    if (nextIndex < 0) {
      nextIndex = items.length - 1;
    } else if (nextIndex >= items.length) {
      nextIndex = 0;
    }
 
    this.focusedIndex.set(nextIndex);
    this.updateItemFocus(items, nextIndex);
  }
 
  private selectFocusedItem(items: HTMLElement[]) {
    const currentIndex = this.focusedIndex();
    if (currentIndex >= 0 && currentIndex < items.length) {
      const item = items[currentIndex];
      const value = item.getAttribute('value');
      const label = item.textContent?.trim() ?? '';
 
      if (value === null || value === undefined) {
        console.warn('No value attribute found on selected item:', item);
        return;
      }
 
      this.selectItem(value, label);
    }
  }
 
  private focusFirstItem(items: HTMLElement[]) {
    if (items.length > 0) {
      this.focusedIndex.set(0);
      this.updateItemFocus(items, 0);
    }
  }
 
  private focusLastItem(items: HTMLElement[]) {
    if (items.length > 0) {
      const lastIndex = items.length - 1;
      this.focusedIndex.set(lastIndex);
      this.updateItemFocus(items, lastIndex);
    }
  }
 
  private updateItemFocus(items: HTMLElement[], focusedIndex: number) {
    for (let index = 0; index < items.length; index++) {
      const item = items[index];
      if (index === focusedIndex) {
        item.focus();
        item.setAttribute('aria-selected', 'true');
        item.setAttribute('data-selected', 'true');
      } else {
        item.removeAttribute('aria-selected');
        item.removeAttribute('data-selected');
      }
    }
  }
 
  private focusDropdown() {
    if (this.overlayRef?.hasAttached()) {
      const dropdownElement = this.overlayRef.overlayElement.querySelector('[role="listbox"]') as HTMLElement;
      if (dropdownElement) {
        dropdownElement.focus();
      }
    }
  }
 
  private focusButton() {
    const button = this.elementRef.nativeElement.querySelector('button');
    if (button) {
      button.focus();
    }
  }
 
  private focusSelectedItem() {
    const items = this.getSelectItems();
    if (items.length === 0) {
      return;
    }
 
    // Find the index of the currently selected item
    let selectedValue;
    if (Array.isArray(this.zValue()) && this.zValue().length) {
      [selectedValue] = this.zValue();
    } else {
      selectedValue = this.zValue();
    }
 
    let selectedIndex = items.findIndex(item => item.getAttribute('value') === selectedValue);
 
    // If no item is selected, focus the first item
    if (selectedIndex === -1) {
      selectedIndex = 0;
    }
 
    this.focusedIndex.set(selectedIndex);
    this.updateItemFocus(items, selectedIndex);
  }
 
  // ControlValueAccessor implementation
  writeValue(value: string | string[] | null): void {
    if (this.zMultiple() && Array.isArray(value)) {
      this.zValue.set(value);
    } else {
      this.zValue.set(value ?? '');
    }
  }
 
  registerOnChange(fn: (value: string) => void): void {
    this.onChange = fn;
  }
 
  registerOnTouched(fn: () => void): void {
    this.onTouched = fn;
  }
 
  setDisabledState(): void {
    // The disabled state is handled by the disabled input
  }
}
 
select.variants.ts
select.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
 
import { mergeClasses } from '../../shared/utils/utils';
 
export const selectVariants = cva(
  'relative inline-block w-full rounded-md group data-active:border data-active:border-ring data-active:ring-ring/50 data-active:ring-[3px]',
);
 
export const selectTriggerVariants = cva(
  mergeClasses(
    'flex w-full items-center justify-between gap-2 rounded-md border border-input bg-transparent',
    'shadow-xs transition-[color,box-shadow] outline-none cursor-pointer disabled:cursor-not-allowed',
    'disabled:opacity-50 data-placeholder:text-muted-foreground [&_svg:not([class*="text-"])]:text-muted-foreground',
    'dark:bg-input/30 dark:hover:bg-input/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40',
    'aria-invalid:border-destructive [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
  ),
  {
    variants: {
      zSize: {
        sm: 'min-h-8 py-1 text-xs px-2',
        default: 'min-h-9 py-1.5 px-3 text-sm',
        lg: 'min-h-10 py-2 text-base px-4',
      },
    },
    defaultVariants: {
      zSize: 'default',
    },
  },
);
export const selectContentVariants = cva(
  'z-9999 min-w-full scrollbar-hide overflow-y-auto rounded-md border bg-popover text-popover-foreground shadow-lg animate-in fade-in-0 zoom-in-95',
);
export const selectItemVariants = cva(
  'relative flex min-w-full cursor-pointer text-nowrap items-center gap-2 rounded-sm mb-0.5 outline-hidden select-none hover:bg-accent hover:text-accent-foreground data-selected:bg-accent data-selected:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:hover:bg-transparent data-disabled:hover:text-current [&_svg:not([class*="text-"])]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*="size-"])]:size-4',
  {
    variants: {
      zSize: {
        sm: 'min-h-8 py-1 text-xs',
        default: 'min-h-9 py-1.5 text-sm',
        lg: 'min-h-10 py-2 text-base',
      },
      zMode: {
        normal: 'pr-8 pl-2',
        compact: 'pl-6.5 pr-2',
      },
    },
    compoundVariants: [
      {
        zMode: 'compact',
        zSize: 'sm',
        class: 'pl-5 pr-2',
      },
    ],
  },
);
 
export const selectItemIconVariants = cva('absolute flex size-3.5 items-center justify-center', {
  variants: {
    // zSize variants are placeholders for compound variant matching
    zSize: {
      sm: '',
      default: '',
      lg: '',
    },
    zMode: {
      normal: 'right-2',
      compact: 'left-2',
    },
  },
  compoundVariants: [
    {
      zMode: 'compact',
      zSize: 'sm',
      class: 'left-1',
    },
  ],
});
 
export type ZardSelectSizeVariants = NonNullable<VariantProps<typeof selectTriggerVariants>['zSize']>;
export type ZardSelectItemModeVariants = NonNullable<VariantProps<typeof selectItemVariants>['zMode']>;
 
select-item.component.ts
select-item.component.ts
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  ElementRef,
  inject,
  input,
  linkedSignal,
  signal,
} from '@angular/core';
 
import {
  selectItemIconVariants,
  selectItemVariants,
  type ZardSelectItemModeVariants,
  type ZardSelectSizeVariants,
} from './select.variants';
import { mergeClasses, transform } from '../../shared/utils/utils';
import { ZardIconComponent } from '../icon/icon.component';
 
// Interface to avoid circular dependency
interface SelectHost {
  selectedValue(): string[];
  selectItem(value: string, label: string): void;
}
 
@Component({
  selector: 'z-select-item, [z-select-item]',
  imports: [ZardIconComponent],
  template: `
    @if (isSelected()) {
      <span [class]="iconClasses()">
        <z-icon zType="check" [zStrokeWidth]="strokeWidth()" aria-hidden="true" />
      </span>
    }
    <span class="truncate">
      <ng-content />
    </span>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
  host: {
    role: 'option',
    tabindex: '-1',
    '[class]': 'classes()',
    '[attr.value]': 'zValue()',
    '[attr.data-disabled]': 'zDisabled() ? "" : null',
    '[attr.data-selected]': 'isSelected() ? "" : null',
    '[attr.aria-selected]': 'isSelected()',
    '(click)': 'onClick()',
  },
})
export class ZardSelectItemComponent {
  readonly elementRef = inject(ElementRef<HTMLElement>);
 
  readonly zValue = input.required<string>();
  readonly zDisabled = input(false, { transform });
  readonly class = input<string>('');
 
  private readonly select = signal<SelectHost | null>(null);
 
  readonly label = linkedSignal<string>(() => {
    const element = this.elementRef.nativeElement;
    return (element.textContent ?? element.innerText)?.trim() ?? '';
  });
 
  readonly zMode = signal<ZardSelectItemModeVariants>('normal');
  readonly zSize = signal<ZardSelectSizeVariants>('default');
 
  protected readonly classes = computed(() =>
    mergeClasses(selectItemVariants({ zMode: this.zMode(), zSize: this.zSize() }), this.class()),
  );
 
  protected readonly iconClasses = computed(() =>
    mergeClasses(selectItemIconVariants({ zMode: this.zMode(), zSize: this.zSize() })),
  );
 
  protected readonly strokeWidth = computed(() => (this.zMode() === 'compact' ? 3 : 2));
 
  protected readonly isSelected = computed(() => this.select()?.selectedValue().includes(this.zValue()) ?? false);
 
  setSelectHost(selectHost: SelectHost) {
    this.select.set(selectHost);
  }
 
  onClick() {
    if (this.zDisabled()) {
      return;
    }
    this.select()?.selectItem(this.zValue(), this.label());
  }
}
 

Examples

default

Selected value:
import { ChangeDetectionStrategy, Component } from '@angular/core';
 
import { ZardBadgeComponent } from '../../badge/badge.component';
import { ZardSelectItemComponent } from '../select-item.component';
import { ZardSelectComponent } from '../select.component';
 
@Component({
  selector: 'z-demo-select-basic',
  imports: [ZardBadgeComponent, ZardSelectComponent, ZardSelectItemComponent],
  template: `
    <div class="flex flex-col gap-4">
      <span>
        Selected value:
        @if (selectedValue) {
          <z-badge>{{ selectedValue }}</z-badge>
        }
      </span>
      <z-select class="w-[300px]" zPlaceholder="Select a fruit" [(zValue)]="selectedValue">
        <z-select-item zValue="apple">Apple</z-select-item>
        <z-select-item zValue="banana">Banana</z-select-item>
        <z-select-item zValue="blueberry">Blueberry</z-select-item>
        <z-select-item zValue="grapes">Grapes</z-select-item>
        <z-select-item zValue="pineapple" zDisabled>Pineapple</z-select-item>
      </z-select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoSelectBasicComponent {
  selectedValue = '';
}
 

multi select

Selected values:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
 
import { ZardBadgeComponent } from '../../badge/badge.component';
import { ZardSelectItemComponent } from '../select-item.component';
import { ZardSelectComponent } from '../select.component';
 
@Component({
  selector: 'z-demo-multi-select-basic',
  imports: [ZardBadgeComponent, ZardSelectComponent, ZardSelectItemComponent],
  template: `
    <div class="flex h-[400px] w-[300px] flex-col gap-4">
      Selected values:
      <span class="flex flex-wrap gap-2">
        @for (value of selectedValues(); track $index) {
          <z-badge> {{ value }} </z-badge>
        }
      </span>
 
      <z-select
        zPlaceholder="Select multiple fruits"
        [zMultiple]="true"
        [zMaxLabelCount]="4"
        [(zValue)]="selectedValues"
      >
        <z-select-item zValue="apple">Apple</z-select-item>
        <z-select-item zValue="banana">Banana</z-select-item>
        <z-select-item zValue="blueberry">Blueberry</z-select-item>
        <z-select-item zValue="grapes">Grapes</z-select-item>
        <z-select-item zValue="pineapple">Pineapple</z-select-item>
        <z-select-item zValue="strawberry">Strawberry</z-select-item>
        <z-select-item zValue="watermelon">Watermelon</z-select-item>
        <z-select-item zValue="kiwi">Kiwi</z-select-item>
        <z-select-item zValue="mango">Mango</z-select-item>
      </z-select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoMultiSelectBasicComponent {
  readonly selectedValues = signal<string[]>([]);
}
 

API

z-select

A customizable select component that allows single value selection.

Input Description Type Default
[class] Custom CSS classes string ''
[zDisabled] Disables the select boolean false
[zLabel] Optional label for the select string ''
[zMaxLabelCount] Limits visible labels in multiselect mode number 1
[zMultiple] Multiselect mode boolean false
[zPlaceholder] Placeholder text string 'Select an option...'
[(zValue)] Selected value string | string[] '' | []
Output Description Payload
(zSelectionChange) Emitted when the selected value changes string | string[]

z-select-item

Represents an individual item inside a z-select component.

Input Description Type Default
[class] Custom CSS classes string ''
[zValue] The value associated with this item string ''
[zDisabled] Disables selection for this item boolean false