Skip to content
Open
42 changes: 41 additions & 1 deletion src/cdk/overlay/overlay.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
The `overlay` package provides a way to open floating panels on the screen.
The `overlay` package provides a way to open floating panels on the screen.

### Initial setup
The CDK overlays depend on a small set of structural styles to work correctly. If you're using
Expand Down Expand Up @@ -87,6 +87,46 @@ strategy will typically inject `ScrollDispatcher` (from `@angular/cdk/scrolling`
of when scrolling takes place. See the documentation for `ScrollDispatcher` for more information
on how scroll events are detected and dispatched.

#### Using the native Popover API
As of Angular v21, the CDK overlay supports rendering overlays as native popover elements instead
of using the traditional overlay container. This uses the browser's native [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API),
which provides improved accessibility, automatic focus management, and better handling of scrolling
and positioning.

To enable the popover behavior, set the `usePopover` option to `true` when creating an overlay:

```ts
const overlayRef = overlay.create({
positionStrategy: this.overlay.position()
.flexibleConnectedTo(trigger)
.withPositions(positions),
usePopover: true,
});
```

When using `FlexibleConnectedPositionStrategy` with popovers, you can also specify the popover
insertion point:

```ts
const overlayRef = overlay.create({
positionStrategy: this.overlay.position()
.flexibleConnectedTo(trigger)
.withPositions(positions)
.withPopoverInsertionPoint(triggerElement),
usePopover: 'auto', // Can also use 'auto', 'manual'
});
```

**Benefits of using the native Popover API:**
- Better accessibility with automatic focus management
- Automatic dismissal on outside click (for auto popovers)
- Improved performance with less CSS in the critical path
- Native browser support for popover stacking
- Better integration with screen readers

**Note:** The popover API is not supported on older browsers. Always provide a fallback or test
browser compatibility before using this feature in production.

### The overlay container
The `OverlayContainer` provides a handle to the container element in which all individual overlay
elements are rendered. By default, the overlay container is appended directly to the document body
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,38 @@
<div class="example-combobox-input-container">
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
<input
#triggerRef
ngComboboxInput
placeholder="Search..."
[(value)]="searchString"
class="example-combobox-input"
/>
</div>

<div popover="manual" #popover class="example-popover">
<ng-template ngComboboxPopupContainer>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
<ng-template
cdkConnectedOverlay
ngComboboxPopupContainer
[cdkConnectedOverlayOrigin]="triggerRef"
[cdkConnectedOverlayOpen]="combobox.expanded()"
[cdkConnectedOverlayUsePopover]="'inline'"
[cdkConnectedOverlayWidth]="panelWidth()"
>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
</div>
}
</div>
</ng-template>
</div>
</div>
}
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,30 @@ import {
signal,
viewChild,
} from '@angular/core';
import {CdkConnectedOverlay} from '@angular/cdk/overlay';

/** @title Combobox with auto-select filtering. */
@Component({
selector: 'combobox-auto-select-example',
templateUrl: 'combobox-auto-select-example.html',
styleUrl: '../combobox-examples.css',
imports: [Combobox, ComboboxInput, ComboboxPopup, ComboboxPopupContainer, Listbox, Option],
imports: [
Combobox,
ComboboxInput,
ComboboxPopup,
ComboboxPopupContainer,
Listbox,
Option,
CdkConnectedOverlay,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComboboxAutoSelectExample {
popover = viewChild<ElementRef>('popover');
listbox = viewChild<Listbox<any>>(Listbox);
combobox = viewChild<Combobox<any>>(Combobox);

panelWidth = signal<number | undefined>(undefined);

searchString = signal('');

options = computed(() =>
Expand All @@ -44,28 +54,17 @@ export class ComboboxAutoSelectExample {

constructor() {
afterRenderEffect(() => {
const popover = this.popover()!;
const combobox = this.combobox()!;
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
if (combobox.expanded()) {
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
this.panelWidth(comboboxRect?.width);
} else {
this.panelWidth(undefined);
}

this.listbox()?.scrollActiveItemIntoView();
});
}

showPopover() {
const popover = this.popover()!;
const combobox = this.combobox()!;

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const popoverEl = popover.nativeElement;

if (comboboxRect) {
popoverEl.style.width = `${comboboxRect.width}px`;
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
popoverEl.style.left = `${comboboxRect.left - 1}px`;
}

popover.nativeElement.showPopover();
}
}

const states = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,38 @@
<div class="example-combobox-input-container">
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
<input
#triggerRef
ngComboboxInput
class="example-combobox-input"
placeholder="Search..."
[(value)]="searchString"
/>
</div>

<div popover="manual" #popover class="example-popover">
<ng-template ngComboboxPopupContainer>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
<ng-template
cdkConnectedOverlay
ngComboboxPopupContainer
[cdkConnectedOverlayOrigin]="triggerRef"
[cdkConnectedOverlayOpen]="combobox.expanded()"
[cdkConnectedOverlayUsePopover]="'inline'"
[cdkConnectedOverlayWidth]="panelWidth()"
>
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
class="example-option example-selectable example-stateful"
ngOption
[value]="option"
[label]="option"
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
<span>{{option}}</span>
<span
aria-hidden="true"
class="material-symbols-outlined example-icon example-selected-icon"
>check</span
>
</div>
}
</div>
</ng-template>
</div>
</div>
}
</div>
</ng-template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
viewChild,
} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {CdkConnectedOverlay} from '@angular/cdk/overlay';

/** @title Disabled combobox example. */
@Component({
Expand All @@ -37,14 +38,16 @@ import {FormsModule} from '@angular/forms';
Listbox,
Option,
FormsModule,
CdkConnectedOverlay,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComboboxDisabledExample {
popover = viewChild<ElementRef>('popover');
listbox = viewChild<Listbox<any>>(Listbox);
combobox = viewChild<Combobox<any>>(Combobox);

panelWidth = signal<number | undefined>(undefined);

searchString = signal('');

options = computed(() =>
Expand All @@ -53,29 +56,17 @@ export class ComboboxDisabledExample {

constructor() {
afterRenderEffect(() => {
const popover = this.popover()!;
const combobox = this.combobox()!;
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
if (combobox.expanded()) {
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
this.panelWidth(comboboxRect?.width);
} else {
this.panelWidth(undefined);
}

this.listbox()?.scrollActiveItemIntoView();
});
}

showPopover() {
const popover = this.popover()!;
const combobox = this.combobox()!;

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const popoverEl = popover.nativeElement;

if (comboboxRect) {
popoverEl.style.width = `${comboboxRect.width}px`;
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
popoverEl.style.left = `${comboboxRect.left - 1}px`;
}

popover.nativeElement.showPopover();
}
}

const states = [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,23 @@
<div class="example-combobox-input-container">
<span class="material-symbols-outlined example-icon example-search-icon">search</span>
<input
#triggerRef
ngComboboxInput
placeholder="Search..."
[(value)]="searchString"
class="example-combobox-input"
/>
</div>

<div popover="manual" #popover class="example-popover">
<ng-template ngComboboxPopupContainer>
<ng-template
cdkConnectedOverlay
[open]="combobox.expanded()"
[hasBackdrop]="false"
[width]="panelWidth()"
[origin]="triggerRef"
ngComboboxPopupContainer
>
<ng-container *ngIf="combobox.expanded()">
<div ngListbox class="example-listbox">
@for (option of options(); track option) {
<div
Expand All @@ -28,6 +36,6 @@
</div>
}
</div>
</ng-template>
</div>
</ng-container>
</ng-template>
</div>
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
viewChild,
} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {CdkConnectedOverlay} from '@angular/cdk/overlay';

/** @title Combobox with highlight filtering. */
@Component({
Expand All @@ -37,14 +38,16 @@ import {FormsModule} from '@angular/forms';
Listbox,
Option,
FormsModule,
CdkConnectedOverlay,
],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ComboboxHighlightExample {
popover = viewChild<ElementRef>('popover');
listbox = viewChild<Listbox<any>>(Listbox);
combobox = viewChild<Combobox<any>>(Combobox);

panelWidth = signal<number | undefined>(undefined);

searchString = signal('');

options = computed(() =>
Expand All @@ -53,29 +56,17 @@ export class ComboboxHighlightExample {

constructor() {
afterRenderEffect(() => {
const popover = this.popover()!;
const combobox = this.combobox()!;
combobox.expanded() ? this.showPopover() : popover.nativeElement.hidePopover();
if (combobox.expanded()) {
const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
this.panelWidth(comboboxRect?.width);
} else {
this.panelWidth(undefined);
}

this.listbox()?.scrollActiveItemIntoView();
});
}

showPopover() {
const popover = this.popover()!;
const combobox = this.combobox()!;

const comboboxRect = combobox.inputElement()?.getBoundingClientRect();
const popoverEl = popover.nativeElement;

if (comboboxRect) {
popoverEl.style.width = `${comboboxRect.width}px`;
popoverEl.style.top = `${comboboxRect.bottom + 4}px`;
popoverEl.style.left = `${comboboxRect.left - 1}px`;
}

popover.nativeElement.showPopover();
}
}

const states = [
Expand Down
Loading