セレクト
概要
読み取り専用コンボボックスとリストボックスを組み合わせて、キーボードナビゲーションとスクリーンリーダーをサポートする単一選択ドロップダウンを作成するパターンです。
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
}
.select {
display: flex;
position: relative;
align-items: center;
color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
border-radius: 0.5rem;
border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 3rem;
height: 2.5rem;
border: none;
}
[ngCombobox]:focus-within .select {
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
margin: 1px;
padding: 0 1rem;
min-height: 2.25rem;
border-radius: 0.5rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="material-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
--primary: var(--hot-pink);
--on-primary: var(--page-background);
}
.docs-light-mode {
--on-primary: #fff;
}
.select {
display: flex;
position: relative;
align-items: center;
border-radius: 3rem;
color: var(--on-primary);
background-color: var(--primary);
border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--primary) 90%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
border: none;
cursor: pointer;
height: 3rem;
padding: 0 3rem;
}
[ngCombobox]:focus-within .select {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 2rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 13rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
padding: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
min-height: 3rem;
border-radius: 3rem;
}
[ngOption]:hover,
[ngOption][data-active='true'] {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid var(--primary);
}
[ngOption][aria-selected='true'] {
color: var(--primary);
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="retro-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:host {
display: flex;
justify-content: center;
font-size: 0.8rem;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--page-background));
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
.select {
display: flex;
position: relative;
align-items: center;
color: var(--page-background);
background-color: var(--hot-pink);
box-shadow: var(--retro-clickable-shadow);
}
.select:hover,
.select:focus-within {
transform: translate(1px, 1px);
}
.select:active {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 4rem;
height: 2.5rem;
border: none;
}
.select:has([ngComboboxInput][aria-expanded='false']):focus-within {
outline-offset: 8px;
outline: 4px dashed var(--hot-pink);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 20px;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
font-size: 0.6rem;
min-height: 2.25rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px dashed var(--hot-pink);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
使い方
selectパターンは、ユーザーがよく知られた選択肢のセットから単一の値を選択する必要がある場合に最適です。
次のような場合にこのパターンの使用を検討してください:
- 選択肢のリストが固定されている場合(20項目未満)- ユーザーはフィルタリングなしで一覧して選択できます
- 選択肢がよく知られている場合 - ユーザーは検索しなくても選択肢を認識できます
- フォームに標準的なフィールドが必要な場合 - 国、州、カテゴリー、またはステータスの選択
- 設定と構成 - 設定やオプションのためのドロップダウンメニュー
- 選択肢のラベルが明確な場合 - 各選択肢に、識別しやすく一覧できる名前が付いている
次のような場合はこのパターンを避けてください:
- リストに20項目以上ある場合 - より良いフィルタリングのためにAutocompleteパターンを使用してください
- ユーザーが選択肢を検索する必要がある場合 - Autocompleteはテキスト入力とフィルタリングを提供します
- 複数選択が必要な場合 - 代わりにMultiselectパターンを使用してください
- 選択肢が非常に少ない場合(2〜3個) - ラジオボタンはすべての選択肢の可視性を高めます
機能
selectパターンは、ComboboxとListboxディレクティブを組み合わせて、以下の機能を備えた完全にアクセシブルなドロップダウンを提供します:
- キーボードナビゲーション - 矢印キーでオプションを移動し、Enterで選択、Escapeで閉じます
- スクリーンリーダーのサポート - 支援技術のための組み込みARIA属性
- カスタム表示 - 選択された値をアイコン、フォーマット、またはリッチコンテンツで表示します
- シグナルベースのリアクティビティ - Angularシグナルを使用したリアクティブな状態管理
- スマートな配置 - CDK Overlayがビューポートの端やスクロールを処理します
- 双方向テキストのサポート - 右から左へ記述する言語 (RTL) を自動的に処理します
例
基本的なセレクト
ユーザーは、値のリストから選択するために標準的なドロップダウンを必要とします。読み取り専用のコンボボックスとリストボックスを組み合わせることで、完全なアクセシビリティサポートを備えた、使い慣れたセレクト体験を提供します。
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox readonly>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
}
.select {
display: flex;
position: relative;
align-items: center;
color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
border-radius: 0.5rem;
border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 1.5rem;
height: 2.5rem;
border: none;
}
[ngCombobox]:focus-within .select {
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
.combobox-label {
gap: 1rem;
left: 1.5rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
margin: 1px;
padding: 0 1rem;
min-height: 2.25rem;
border-radius: 0.5rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="material-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
--primary: var(--hot-pink);
--on-primary: var(--page-background);
}
.docs-light-mode {
--on-primary: #fff;
}
.select {
display: flex;
position: relative;
align-items: center;
border-radius: 3rem;
color: var(--on-primary);
background-color: var(--primary);
border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--primary) 90%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
[ngComboboxInput] {
opacity: 0;
border: none;
cursor: pointer;
height: 3rem;
padding: 0 1.5rem;
}
[ngCombobox]:focus-within .select {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.combobox-label {
gap: 1rem;
left: 1.5rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 2rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 13rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
padding: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
min-height: 3rem;
border-radius: 3rem;
}
[ngOption]:hover,
[ngOption][data-active='true'] {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid var(--primary);
}
[ngOption][aria-selected='true'] {
color: var(--primary);
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="retro-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:host {
display: flex;
justify-content: center;
font-size: 0.8rem;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--page-background));
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
.select {
display: flex;
position: relative;
align-items: center;
color: var(--page-background);
background-color: var(--hot-pink);
box-shadow: var(--retro-clickable-shadow);
}
.select:hover,
.select:focus-within {
transform: translate(1px, 1px);
}
.select:active {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 3.5rem;
height: 2.5rem;
border: none;
}
.select:has([ngComboboxInput][aria-expanded='false']):focus-within {
outline-offset: 8px;
outline: 4px dashed var(--hot-pink);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 20px;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
font-size: 0.6rem;
min-height: 2.25rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px dashed var(--hot-pink);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
ngComboboxのreadonly属性は、キーボードナビゲーションを維持しながらテキスト入力を防ぎます。ユーザーは、ネイティブのselect要素と同じように、矢印キーとEnterキーを使用してドロップダウンを操作します。
カスタム表示のセレクト
オプションには、ユーザーが選択肢をすばやく識別できるように、アイコンやバッジなどの視覚的なインジケーターが必要になることがよくあります。オプション内のカスタムテンプレートを使用すると、アクセシビリティを維持しながらリッチなフォーマットが可能になります。
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
}
.select {
display: flex;
position: relative;
align-items: center;
color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
border-radius: 0.5rem;
border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--hot-pink) 15%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 3rem;
height: 2.5rem;
border: none;
}
[ngCombobox]:focus-within .select {
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
margin: 1px;
padding: 0 1rem;
min-height: 2.25rem;
border-radius: 0.5rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="material-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
--primary: var(--hot-pink);
--on-primary: var(--page-background);
}
.docs-light-mode {
--on-primary: #fff;
}
.select {
display: flex;
position: relative;
align-items: center;
border-radius: 3rem;
color: var(--on-primary);
background-color: var(--primary);
border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);
}
.select:hover {
background-color: color-mix(in srgb, var(--primary) 90%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
border: none;
cursor: pointer;
height: 3rem;
padding: 0 3rem;
}
[ngCombobox]:focus-within .select {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 2rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 13rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
padding: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
min-height: 3rem;
border-radius: 3rem;
}
[ngOption]:hover,
[ngOption][data-active='true'] {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid var(--primary);
}
[ngOption][aria-selected='true'] {
color: var(--primary);
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
signal,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The icon that is displayed in the combobox. */
displayIcon = computed(() => {
const values = this.listbox()?.values() || [];
const label = this.labels.find((label) => label.value === values[0]);
return label ? label.icon : '';
});
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = [
{value: 'Important', icon: 'label'},
{value: 'Starred', icon: 'star'},
{value: 'Work', icon: 'work'},
{value: 'Personal', icon: 'person'},
{value: 'To Do', icon: 'checklist'},
{value: 'Later', icon: 'schedule'},
{value: 'Read', icon: 'menu_book'},
{value: 'Travel', icon: 'flight'},
];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="retro-select" readonly>
<div #origin class="select">
<span class="combobox-label">
<span
class="selected-label-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ displayIcon() }}</span
>
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label.value) {
<div ngOption [value]="label.value" [label]="label.value">
<span
class="example-option-icon material-symbols-outlined"
translate="no"
aria-hidden="true"
>{{ label.icon }}</span
>
<span class="example-option-text">{{ label.value }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:host {
display: flex;
justify-content: center;
font-size: 0.8rem;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--page-background));
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
.select {
display: flex;
position: relative;
align-items: center;
color: var(--page-background);
background-color: var(--hot-pink);
box-shadow: var(--retro-clickable-shadow);
}
.select:hover,
.select:focus-within {
transform: translate(1px, 1px);
}
.select:active {
transform: translate(4px, 4px);
box-shadow: var(--retro-pressed-shadow);
background-color: color-mix(in srgb, var(--retro-button-color) 60%, var(--gray-50));
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
cursor: default;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 4rem;
height: 2.5rem;
border: none;
}
.select:has([ngComboboxInput][aria-expanded='false']):focus-within {
outline-offset: 8px;
outline: 4px dashed var(--hot-pink);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 20px;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
font-size: 0.6rem;
min-height: 2.25rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px dashed var(--hot-pink);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
各オプションには、ラベルの横にアイコンが表示されます。選択された値は、選択されたオプションのアイコンとテキストを表示するように更新され、明確な視覚的フィードバックを提供します。
無効化されたセレクト
特定のフォーム条件が満たされていない場合にユーザーの操作を防ぐために、セレクトを無効にできます。無効状態は、視覚的なフィードバックを提供し、キーボード操作を防ぎます。
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox readonly disabled>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
}
.select {
display: flex;
position: relative;
align-items: center;
color: color-mix(in srgb, var(--hot-pink) 90%, var(--primary-contrast));
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
border-radius: 0.5rem;
border: 1px solid color-mix(in srgb, var(--hot-pink) 80%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 1.5rem;
height: 2.5rem;
border: none;
}
[ngComboboxInput][aria-disabled='true'] {
cursor: default;
}
[ngCombobox]:focus-within .select {
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
.combobox-label {
gap: 1rem;
left: 1.5rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 0.5rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
margin: 1px;
padding: 0 1rem;
min-height: 2.25rem;
border-radius: 0.5rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid color-mix(in srgb, var(--hot-pink) 50%, transparent);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="material-select" readonly disabled>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
:host {
display: flex;
justify-content: center;
font-family: var(--inter-font);
--primary: var(--hot-pink);
--on-primary: var(--page-background);
}
.docs-light-mode {
--on-primary: #fff;
}
.select {
display: flex;
position: relative;
align-items: center;
border-radius: 3rem;
color: var(--on-primary);
background-color: var(--primary);
border: 1px solid color-mix(in srgb, var(--primary) 80%, transparent);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
}
[ngComboboxInput] {
opacity: 0;
border: none;
cursor: pointer;
height: 3rem;
padding: 0 1.5rem;
}
[ngComboboxInput][aria-disabled='true'] {
cursor: default;
}
[ngCombobox]:focus-within .select {
outline: 2px solid var(--primary);
outline-offset: 2px;
}
.combobox-label {
gap: 1rem;
left: 1.5rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 8px;
border-radius: 2rem;
background-color: var(--septenary-contrast);
font-size: 0.9rem;
max-height: 13rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
padding: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
min-height: 3rem;
border-radius: 3rem;
}
[ngOption]:hover,
[ngOption][data-active='true'] {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px solid var(--primary);
}
[ngOption][aria-selected='true'] {
color: var(--primary);
background-color: color-mix(in srgb, var(--primary) 10%, transparent);
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
app.ts
import {
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
} from '@angular/aria/combobox';
import {Listbox, Option} from '@angular/aria/listbox';
import {
afterRenderEffect,
ChangeDetectionStrategy,
Component,
computed,
viewChild,
viewChildren,
} from '@angular/core';
import {OverlayModule} from '@angular/cdk/overlay';
@Component({
selector: 'app-root',
templateUrl: './app.html',
styleUrl: './app.css',
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
OverlayModule,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
/** The combobox listbox popup. */
listbox = viewChild<Listbox<string>>(Listbox);
/** The options available in the listbox. */
options = viewChildren<Option<string>>(Option);
/** A reference to the ng aria combobox. */
combobox = viewChild<Combobox<string>>(Combobox);
/** The string that is displayed in the combobox. */
displayValue = computed(() => {
const values = this.listbox()?.values() || [];
return values.length ? values[0] : 'Select a label';
});
/** The labels that are available for selection. */
labels = ['Important', 'Starred', 'Work', 'Personal', 'To Do', 'Later', 'Read', 'Travel'];
constructor() {
// Scrolls to the active item when the active option changes.
// The slight delay here is to ensure animations are done before scrolling.
afterRenderEffect(() => {
const option = this.options().find((opt) => opt.active());
setTimeout(() => option?.element.scrollIntoView({block: 'nearest'}), 50);
});
// Resets the listbox scroll position when the combobox is closed.
afterRenderEffect(() => {
if (!this.combobox()?.expanded()) {
setTimeout(() => this.listbox()?.element.scrollTo(0, 0), 150);
}
});
}
}
app.html
<div ngCombobox class="retro-select" readonly disabled>
<div #origin class="select">
<span class="combobox-label">
<span class="selected-label-text">{{ displayValue() }}</span>
</span>
<input aria-label="Label dropdown" placeholder="Select a label" ngComboboxInput />
<span class="example-arrow material-symbols-outlined" translate="no" aria-hidden="true"
>arrow_drop_down</span
>
</div>
<ng-template ngComboboxPopupContainer>
<ng-template
[cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: true}"
[cdkConnectedOverlayOpen]="true"
>
<div class="example-popup-container">
<div ngListbox>
@for (label of labels; track label) {
<div ngOption [value]="label" [label]="label">
<span class="example-option-text">{{ label }}</span>
<span
class="example-option-check material-symbols-outlined"
translate="no"
aria-hidden="true"
>check</span
>
</div>
}
</div>
</div>
</ng-template>
</ng-template>
</div>
app.css
@import url('https://fonts.googleapis.com/icon?family=Material+Symbols+Outlined');
@import url('https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap');
:host {
display: flex;
justify-content: center;
font-size: 0.8rem;
font-family: 'Press Start 2P';
--retro-button-color: color-mix(in srgb, var(--hot-pink) 80%, var(--page-background));
--retro-shadow-light: color-mix(in srgb, var(--retro-button-color) 90%, #fff);
--retro-shadow-dark: color-mix(in srgb, var(--retro-button-color) 90%, #000);
--retro-elevated-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast);
--retro-flat-shadow:
4px 0px 0px 0px var(--tertiary-contrast), 0px 4px 0px 0px var(--tertiary-contrast),
-4px 0px 0px 0px var(--tertiary-contrast), 0px -4px 0px 0px var(--tertiary-contrast);
--retro-clickable-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-light),
inset -4px -4px 0px 0px var(--retro-shadow-dark), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 8px 8px 0px 0px var(--tertiary-contrast);
--retro-pressed-shadow:
inset 4px 4px 0px 0px var(--retro-shadow-dark),
inset -4px -4px 0px 0px var(--retro-shadow-light), 4px 0px 0px 0px var(--tertiary-contrast),
0px 4px 0px 0px var(--tertiary-contrast), -4px 0px 0px 0px var(--tertiary-contrast),
0px -4px 0px 0px var(--tertiary-contrast), 0px 0px 0px 0px var(--tertiary-contrast);
}
.select {
display: flex;
position: relative;
align-items: center;
color: var(--page-background);
background-color: var(--hot-pink);
box-shadow: var(--retro-clickable-shadow);
}
.select:has([ngComboboxInput][aria-disabled='true']) {
opacity: 0.6;
}
.selected-label-icon {
font-size: 1.25rem;
}
[ngComboboxInput] {
opacity: 0;
cursor: pointer;
padding: 0 3.5rem;
height: 2.5rem;
border: none;
}
[ngComboboxInput][aria-disabled='true'] {
cursor: default;
}
.select:has([ngComboboxInput][aria-expanded='false']):focus-within {
outline-offset: 8px;
outline: 4px dashed var(--hot-pink);
}
.combobox-label {
gap: 1rem;
left: 1rem;
display: flex;
position: absolute;
align-items: center;
pointer-events: none;
}
.example-arrow {
right: 1rem;
position: absolute;
pointer-events: none;
transition: transform 150ms ease-in-out;
}
[ngComboboxInput][aria-expanded='true'] ~ .example-arrow {
transform: rotate(180deg);
}
.example-popup-container {
width: 100%;
padding: 0.5rem;
margin-top: 20px;
box-shadow: var(--retro-flat-shadow);
background-color: var(--septenary-contrast);
max-height: 11rem;
opacity: 1;
visibility: visible;
transition:
max-height 150ms ease-out,
visibility 0s,
opacity 25ms ease-out;
}
[ngListbox] {
gap: 2px;
height: 100%;
display: flex;
overflow: auto;
flex-direction: column;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='false']) .example-popup-container {
max-height: 0;
opacity: 0;
visibility: hidden;
transition:
max-height 150ms ease-in,
visibility 0s 150ms,
opacity 150ms ease-in;
}
[ngCombobox]:has([ngComboboxInput][aria-expanded='true']) [ngListbox] {
display: flex;
}
[ngOption] {
display: flex;
cursor: pointer;
align-items: center;
padding: 0 1rem;
font-size: 0.6rem;
min-height: 2.25rem;
}
[ngOption]:hover {
background-color: color-mix(in srgb, var(--primary-contrast) 5%, transparent);
}
[ngOption][data-active='true'] {
outline-offset: -2px;
outline: 2px dashed var(--hot-pink);
}
[ngOption][aria-selected='true'] {
color: var(--hot-pink);
background-color: color-mix(in srgb, var(--hot-pink) 5%, transparent);
}
.example-option-icon {
font-size: 1.25rem;
padding-right: 1rem;
}
[ngOption]:not([aria-selected='true']) .example-option-check {
display: none;
}
.example-option-icon,
.example-option-check {
font-size: 0.9rem;
}
.example-option-text {
flex: 1;
}
無効にすると、セレクトは無効の視覚的状態を示し、すべてのユーザー操作をブロックします。スクリーンリーダーは、支援技術のユーザーに無効状態をアナウンスします。
API
selectパターンは、AngularのAriaライブラリから以下のディレクティブを使用します。詳細なAPIドキュメントについては、リンク先のガイドを参照してください。
コンボボックスディレクティブ
selectパターンは、キーボードナビゲーションを維持しつつテキスト入力を防ぐために、readonly属性を持つngComboboxを使用します。
入力
| プロパティ | 型 | デフォルト | 説明 |
|---|---|---|---|
readonly |
boolean |
false |
trueに設定すると、ドロップダウンの動作になります |
disabled |
boolean |
false |
select全体を無効にします |
利用可能なすべての入力とシグナルの詳細については、コンボボックスAPIドキュメントを参照してください。
リストボックスディレクティブ
selectパターンは、ドロップダウンリストにngListboxを、選択可能な各項目にngOptionを使用します。
モデル
| プロパティ | 型 | 説明 |
|---|---|---|
values |
any[] |
選択された値の双方向バインディング可能な配列(selectの場合は単一の値を含む) |
リストボックスの設定、選択モード、およびオプションのプロパティに関する完全な詳細については、リストボックスAPIドキュメントを参照してください。
ポジショニング
selectパターンは、スマートなポジショニングのためにCDK Overlayと統合されています。ビューポートの端やスクロールを自動的に処理するにはcdkConnectedOverlayを使用してください。