Select

A custom dropdown for picking one value, built for Angular forms.

import { โ€ฆ } from '@inlaylab/ivorie/select'

Value:

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-3">
      <iv-select [(value)]="fruit" placeholder="Pick a fruit">
        <iv-select-trigger class="w-48" aria-label="Fruit" />
        <iv-select-content>
          <iv-select-item value="apple">Apple</iv-select-item>
          <iv-select-item value="banana">Banana</iv-select-item>
          <iv-select-item value="orange">Orange</iv-select-item>
        </iv-select-content>
      </iv-select>
      <p class="text-muted-foreground text-sm">Value: {{ fruit() }}</p>
    </div>
  `,
})
export default class SelectDemo {
  protected readonly fruit = signal<string | null>(null);
}

Installation

CLInpm
npx ivorie add select

A styled dropdown, not a native <select>. It supports ngModel, reactive forms, groups, labels and separators.

Usage

import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';
<iv-select [(value)]="fruit" placeholder="Pick a fruit">
  <iv-select-trigger />
  <iv-select-content>
    <iv-select-item value="apple">Apple</iv-select-item>
    <iv-select-item value="banana">Banana</iv-select-item>
  </iv-select-content>
</iv-select>

Examples

With ngModel

Value: banana

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-ngmodel',
  imports: [FormsModule, IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="flex flex-col items-center gap-3">
      <iv-select [(ngModel)]="fruit" placeholder="Pick a fruit">
        <iv-select-trigger class="w-48" aria-label="Fruit" />
        <iv-select-content>
          <iv-select-item value="apple">Apple</iv-select-item>
          <iv-select-item value="banana">Banana</iv-select-item>
          <iv-select-item value="orange">Orange</iv-select-item>
        </iv-select-content>
      </iv-select>
      <p class="text-muted-foreground text-sm">Value: {{ fruit() }}</p>
    </div>
  `,
})
export default class SelectNgmodelDemo {
  protected readonly fruit = signal<string | null>('banana');
}

Reactive forms

Value: apple

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule } from '@angular/forms';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-reactive-forms',
  imports: [ReactiveFormsModule, IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <form [formGroup]="form" class="flex flex-col items-center gap-3">
      <iv-select formControlName="fruit" placeholder="Pick a fruit">
        <iv-select-trigger class="w-48" aria-label="Fruit" />
        <iv-select-content>
          <iv-select-item value="apple">Apple</iv-select-item>
          <iv-select-item value="banana">Banana</iv-select-item>
          <iv-select-item value="orange">Orange</iv-select-item>
        </iv-select-content>
      </iv-select>
      <p class="text-muted-foreground text-sm">Value: {{ form.value.fruit }}</p>
    </form>
  `,
})
export default class SelectReactiveFormsDemo {
  protected readonly form = new FormGroup({ fruit: new FormControl<string | null>('apple') });
}

Groups

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-groups',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-select [(value)]="city" placeholder="Pick a city">
      <iv-select-trigger class="w-56" aria-label="City" />
      <iv-select-content>
        <iv-select-group>
          <iv-select-label>North America</iv-select-label>
          <iv-select-item value="nyc">New York</iv-select-item>
          <iv-select-item value="la">Los Angeles</iv-select-item>
        </iv-select-group>
        <iv-select-separator />
        <iv-select-group>
          <iv-select-label>Europe</iv-select-label>
          <iv-select-item value="lon">London</iv-select-item>
          <iv-select-item value="par">Paris</iv-select-item>
        </iv-select-group>
      </iv-select-content>
    </iv-select>
  `,
})
export default class SelectGroupsDemo {
  protected readonly city = signal<string | null>(null);
}

Small

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-small',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-select [(value)]="fruit" size="sm" placeholder="Pick a fruit">
      <iv-select-trigger class="w-48" aria-label="Fruit" />
      <iv-select-content>
        <iv-select-item value="apple">Apple</iv-select-item>
        <iv-select-item value="banana">Banana</iv-select-item>
        <iv-select-item value="orange">Orange</iv-select-item>
      </iv-select-content>
    </iv-select>
  `,
})
export default class SelectSmallDemo {
  protected readonly fruit = signal<string | null>(null);
}

Disabled

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-disabled',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-select value="apple" [disabled]="true">
      <iv-select-trigger class="w-48" aria-label="Fruit" />
      <iv-select-content>
        <iv-select-item value="apple">Apple</iv-select-item>
        <iv-select-item value="banana">Banana</iv-select-item>
      </iv-select-content>
    </iv-select>
  `,
})
export default class SelectDisabledDemo {}

Disabled item

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-disabled-item',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-select [(value)]="plan" placeholder="Pick a plan">
      <iv-select-trigger class="w-52" aria-label="Plan" />
      <iv-select-content>
        <iv-select-item value="free">Free</iv-select-item>
        <iv-select-item value="pro">Pro</iv-select-item>
        <iv-select-item value="enterprise" [disabled]="true">Enterprise (unavailable)</iv-select-item>
      </iv-select-content>
    </iv-select>
  `,
})
export default class SelectDisabledItemDemo {
  protected readonly plan = signal<string | null>(null);
}

Filter label

import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { IV_SELECT_IMPORTS } from '@inlaylab/ivorie/select';

@Component({
  selector: 'demo-select-filter-label',
  imports: [IV_SELECT_IMPORTS],
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <iv-select [(value)]="status" placeholder="All">
      <iv-select-trigger label="Status" resetValue="all" class="w-52" aria-label="Status filter" />
      <iv-select-content>
        <iv-select-item value="all">All</iv-select-item>
        <iv-select-item value="active">Active</iv-select-item>
        <iv-select-item value="archived">Archived</iv-select-item>
      </iv-select-content>
    </iv-select>
  `,
})
export default class SelectFilterLabelDemo {
  protected readonly status = signal<string | null>('active');
}

API

Every part also accepts class.

iv-select

Name Type Default Description
value string | null null Selected value. Bind with [(value)].
placeholder string 'Select an option' Shown when no item matches the value.
disabled boolean false Disables the trigger. Also disabled when the form control is disabled.
size 'default' | 'sm' 'default' Trigger size.

Outputs:

Name Payload Description
valueChange string | null Emits when the user selects an item.

iv-select-trigger

Renders a native button. class goes on that button. The trigger is w-fit; add class="w-full" to fill its container. A truncated value shows its full text in a tooltip on hover.

Name Type Default Description
id string | null null Set on the inner button, so <label for> works.
aria-label string | null null Set on the inner button.
aria-labelledby string | null null Set on the inner button.
size 'default' | 'sm' | undefined undefined Overrides the root size.
label string '' Static prefix before the value, such as Feature: All. For filter bars.
resetValue string '' Value that means "no filter". With label set, any other value marks the trigger active (data-active).

iv-select-content

The dropdown panel, shown in a CDK overlay. No inputs besides class.

iv-select-group

Wraps related items. Has role="group". No inputs besides class.

iv-select-label

Muted heading for a group. No inputs besides class.

iv-select-item

Name Type Default Description
value string required Value set on selection.
disabled boolean false Blocks selection of this item.

iv-select-separator

A divider line. Hidden from assistive tech. No inputs besides class.

Accessibility

  • Focus stays on the trigger. It has role="combobox", aria-expanded, aria-controls and aria-activedescendant.
  • The list has role="listbox". Items have role="option", aria-selected and aria-disabled.
  • Enter, Space, ArrowDown and ArrowUp open the list on the selected item, else the first.
  • While open: arrows, Home, End and typeahead move the active option and skip disabled items.
  • Enter or Space selects and closes. Escape closes and returns focus to the trigger. Tab closes.
  • Escape is handled, so an enclosing dialog stays open.

Notes

  • The trigger shows the text of the item whose value matches. Items added later (for example from async data) update it. No match shows the placeholder.
  • The list is a CDK overlay, so parent overflow: hidden does not clip it. A click outside closes it.
  • Closing marks the form control touched.
  • ngModel and formControlName need FormsModule or ReactiveFormsModule.