native select

A styled native HTML select element with consistent design system integration.

PreviousNext
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-preview',
  imports: [ZardNativeSelectDirective, ...ZardFieldImports],
  template: `
    <div z-field class="w-64">
      <label z-field-label for="native-select-status">Status</label>
      <select z-native-select id="native-select-status">
        <option value="" disabled selected>Select status</option>
        <option value="todo">Todo</option>
        <option value="in-progress">In progress</option>
        <option value="done">Done</option>
        <option value="cancelled">Cancelled</option>
      </select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectPreviewComponent {}

Installation

Copy
npx zard-cli@latest add native-select

Usage

import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';
Copy
<select z-native-select>
  <option value="todo">Todo</option>
  <option value="in-progress">In progress</option>
  <option value="done">Done</option>
</select>
Copy

Examples

groups

Use plain <optgroup> elements to organize options into categories.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-groups',
  imports: [ZardNativeSelectDirective, ...ZardFieldImports],
  template: `
    <div z-field class="w-64">
      <label z-field-label for="native-select-department">Department</label>
      <select z-native-select id="native-select-department">
        <option value="" disabled selected>Select department</option>
        <optgroup label="Engineering">
          <option value="frontend">Frontend</option>
          <option value="backend">Backend</option>
          <option value="devops">DevOps</option>
        </optgroup>
        <optgroup label="Sales">
          <option value="account-executive">Account executive</option>
          <option value="sales-development">Sales development</option>
        </optgroup>
        <optgroup label="Operations">
          <option value="support">Customer support</option>
          <option value="product-manager">Product manager</option>
        </optgroup>
      </select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectGroupsComponent {}

sizes

Use zSize to pick between sm, default and lg. The default height lines up with z-input.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-sizes',
  imports: [ZardNativeSelectDirective],
  template: `
    <div class="flex w-64 flex-col gap-4">
      <select z-native-select zSize="sm" aria-label="Small">
        <option>Small</option>
        <option>Option two</option>
      </select>
      <select z-native-select aria-label="Default">
        <option>Default</option>
        <option>Option two</option>
      </select>
      <select z-native-select zSize="lg" aria-label="Large">
        <option>Large</option>
        <option>Option two</option>
      </select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectSizesComponent {}

disabled

Set the native disabled attribute on select[z-native-select], and add data-disabled="true" to the surrounding z-field so its label dims along with the control.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-disabled',
  imports: [ZardNativeSelectDirective, ...ZardFieldImports],
  template: `
    <div z-field class="w-64" data-disabled="true">
      <label z-field-label for="native-select-disabled">Status</label>
      <select z-native-select id="native-select-disabled" disabled>
        <option value="todo">Todo</option>
        <option value="in-progress">In progress</option>
        <option value="done">Done</option>
      </select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectDisabledComponent {}

invalid

Set aria-invalid="true" on select[z-native-select] and data-invalid="true" on the surrounding z-field to show the invalid state.
Select a status to continue.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-invalid',
  imports: [ZardNativeSelectDirective, ...ZardFieldImports],
  template: `
    <div z-field class="w-64" data-invalid="true">
      <label z-field-label for="native-select-invalid">Status</label>
      <select z-native-select id="native-select-invalid" aria-invalid="true">
        <option value="" disabled selected>Select status</option>
        <option value="todo">Todo</option>
        <option value="in-progress">In progress</option>
        <option value="done">Done</option>
      </select>
      <z-field-error>Select a status to continue.</z-field-error>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectInvalidComponent {}

form

The directive adds no ControlValueAccessor: Angular binds formControl, formControlName and ngModel to the native <select> as usual.

Current value: none

import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormControl, FormGroup, ReactiveFormsModule, Validators } from '@angular/forms';

import { ZardButtonComponent } from '@/shared/components/button/button.component';
import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';

@Component({
  selector: 'z-demo-native-select-form',
  imports: [ZardNativeSelectDirective, ZardButtonComponent, ReactiveFormsModule, ...ZardFieldImports],
  template: `
    <form class="w-64" [formGroup]="form" (ngSubmit)="onSubmit()">
      <div z-field-group>
        @let statusControl = form.controls.status;
        @let statusInvalid = statusControl.invalid && statusControl.touched;
        <div z-field [attr.data-invalid]="statusInvalid || null">
          <label z-field-label for="native-select-form-status">Status</label>
          <select
            z-native-select
            id="native-select-form-status"
            formControlName="status"
            [attr.aria-invalid]="statusInvalid || null"
          >
            <option value="" disabled>Select status</option>
            <option value="todo">Todo</option>
            <option value="in-progress">In progress</option>
            <option value="done">Done</option>
          </select>
          @if (statusInvalid) {
            <z-field-error>Status is required.</z-field-error>
          } @else {
            <p z-field-description>Current value: {{ statusControl.value || 'none' }}</p>
          }
        </div>
        <div z-field zOrientation="horizontal">
          <button z-button type="button" zType="outline" (click)="form.reset()">Reset</button>
          <button z-button type="submit">Submit</button>
        </div>
      </div>
    </form>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectFormComponent {
  readonly form = new FormGroup({
    status: new FormControl('', { nonNullable: true, validators: [Validators.required] }),
  });

  onSubmit(): void {
    this.form.markAllAsTouched();
  }
}

native vs select

Native Select keeps the browser picker, keyboard handling and form behavior. Select is an overlay with custom items, search and templates.
import { ChangeDetectionStrategy, Component } from '@angular/core';

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';
import { ZardSelectImports } from '@/shared/components/select/select.imports';

@Component({
  selector: 'z-demo-native-select-native-vs-select',
  imports: [ZardNativeSelectDirective, ZardSelectImports, ...ZardFieldImports],
  template: `
    <div class="flex w-full max-w-md flex-col gap-4 sm:flex-row">
      <div z-field>
        <label z-field-label for="native-select-compare-native">Native Select</label>
        <select z-native-select id="native-select-compare-native">
          <option value="apple">Apple</option>
          <option value="banana">Banana</option>
          <option value="blueberry">Blueberry</option>
        </select>
      </div>
      <div z-field>
        <label z-field-label for="native-select-compare-select">Select</label>
        <z-select id="native-select-compare-select" zPlaceholder="Select a fruit">
          <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>
      </div>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectNativeVsSelectComponent {}

rtl

To enable RTL support in zard/ui, see the RTL configuration guide. The chevron moves to the start side.
// You will notice this example is rendered inside a wrapper that sets the dir and data-lang attributes, and
// reads its text from a translations helper. This is because this site is not RTL by default.
// In your application you won't need either: set dir="rtl" on <html> (or use the CDK Dir directive on a subtree)
// and write the text of your own language. See the RTL guide: https://zardui.com/docs/rtl

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

import { ZardFieldImports } from '@/shared/components/field/field.imports';
import { ZardNativeSelectDirective } from '@/shared/components/native-select/native-select.directive';
import { injectRtlTranslation, type ZardRtlTranslations } from '@/shared/demo-utils/rtl-translation';

interface NativeSelectRtlValues {
  label: string;
  placeholder: string;
  todo: string;
  inProgress: string;
  done: string;
}

const translations: ZardRtlTranslations<NativeSelectRtlValues> = {
  en: {
    dir: 'ltr',
    values: { label: 'Status', placeholder: 'Select status', todo: 'Todo', inProgress: 'In progress', done: 'Done' },
  },
  ar: {
    dir: 'rtl',
    values: {
      label: 'الحالة',
      placeholder: 'اختر الحالة',
      todo: 'قيد الانتظار',
      inProgress: 'قيد التنفيذ',
      done: 'مكتمل',
    },
  },
  he: {
    dir: 'rtl',
    values: { label: 'סטטוס', placeholder: 'בחר סטטוס', todo: 'לביצוע', inProgress: 'בתהליך', done: 'הושלם' },
  },
};

@Component({
  selector: 'z-demo-native-select-rtl',
  imports: [ZardNativeSelectDirective, ...ZardFieldImports],
  template: `
    <div z-field class="w-64">
      <label z-field-label for="native-select-rtl">{{ i18n.t().label }}</label>
      <select z-native-select id="native-select-rtl">
        <option value="" disabled selected>{{ i18n.t().placeholder }}</option>
        <option value="todo">{{ i18n.t().todo }}</option>
        <option value="in-progress">{{ i18n.t().inProgress }}</option>
        <option value="done">{{ i18n.t().done }}</option>
      </select>
    </div>
  `,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ZardDemoNativeSelectRtlComponent {
  protected readonly i18n = injectRtlTranslation(translations);
}

API Reference

select[z-native-select]

Styles a native <select>; forms and native attributes work as usual.

Prop
[zSize]

Size; default matches z-input

Type
'sm' | 'default' | 'lg'
Default
'default'
[class]

Custom CSS classes

Type
ClassValue
Default
''
github iconwhatsapp icondiscord iconX icon

Made with ❤ in Brazil. Open source and available on GitHub .