# Overlay

- Entry point: `@koobiq/components/loader-overlay`
- NgModule: `KbqLoaderOverlayModule`
- Page: https://koobiq.io/en/components/loader-overlay/overview

## Overview

The loading overlay displays an ongoing process within a block.

### When to use

When you need to clearly show that a block is loading or a process is running. Before adding a loader overlay, clarify the loading time.

### Component structure

- Semi-transparent backdrop
- Spinner
- Main text (optional)
- Caption (optional)

**Example: Loader-overlay** (`loader-overlay-overview`)

```ts loader-overlay-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';

@Component({
    selector: 'loader-overlay-overview-example',
    imports: [KbqLoaderOverlayModule],
    template: `
        <div class="flex" style="height: 320px">
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text
            <kbq-loader-overlay [caption]="'18,7 МБ из 25 МБ — осталось 2 мин'" [text]="'Создание отчета'" />
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayOverviewExample {}
```

### How it works

The spinner overlay is a temporary state of a block. It appears in response to a user action (for example, clicking a submit button in a form) or a system process. After the process completes, the overlay and spinner disappear.

Elements behind the overlay must not be accessible for focus, selection, or clicking.

#### Default state

Semi-transparent overlay, spinner, main text.
The default text is "Loading"; it is recommended to choose the text individually.
Do not use single-word "Loading" text with an enlarged spinner.

**Example: Loader-overlay default** (`loader-overlay-default`)

```ts loader-overlay-default-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';

@Component({
    selector: 'loader-overlay-default-example',
    imports: [KbqLoaderOverlayModule],
    template: `
        <div class="flex" style="height: 320px">
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text
            <kbq-loader-overlay [text]="'Загрузка'" />
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayDefaultExample {}
```

#### Focus and keyboard navigation

The system prevents focusing on form elements covered by the overlay. The content behind the overlay cannot be selected or clicked either, and screen readers skip its elements. The overlay itself stays available: its text can be selected, and buttons and links on it, such as Cancel or a link to help, work as usual. Screen readers do not announce the overlay when it appears, so announce the loading separately, for example with `LiveAnnouncer` from `@angular/cdk/a11y`.

Once the overlay is hidden, focus returns to the element that had it before the overlay appeared, unless the user has moved it to another element. To move focus to another element behind the overlay after loading, do it once the overlay is hidden, for example in `afterNextRender`.

#### Display delay

If a request completes quickly, the spinner only flashes, which looks like a glitch. Show the overlay after 500 ms: a fast request completes earlier, and the overlay never appears. Once the overlay has appeared, keep it on screen for at least 300 ms.

Delay only the indicator: block a repeated action immediately, for example by disabling the button. Use the `kbqDelayLoadingIndicator` operator for the delay.

The delay does not prevent content from jumping. To keep content in place, keep the previous data until the new data arrives, reserve space for the block, or show a [skeleton](https://koobiq.io/en/components/skeleton.md): it appears immediately, without a delay.

**Example: Loader-overlay delay** (`loader-overlay-delay`)

```ts loader-overlay-delay-example.ts
import { ChangeDetectionStrategy, Component, DestroyRef, inject, model, signal } from '@angular/core';
import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { kbqDelayLoadingIndicator } from '@koobiq/components/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';
import { KbqToggleModule } from '@koobiq/components/toggle';
import { switchMap, timer } from 'rxjs';

@Component({
    selector: 'loader-overlay-delay-example',
    imports: [KbqLoaderOverlayModule, KbqButtonModule, KbqButtonToggleModule, KbqToggleModule, FormsModule],
    template: `
        <div class="example__controls">
            <kbq-toggle [(ngModel)]="delay">Display delay</kbq-toggle>

            <kbq-button-toggle-group aria-label="Request duration" [(ngModel)]="duration">
                <kbq-button-toggle [value]="200">200 ms</kbq-button-toggle>
                <kbq-button-toggle [value]="600">600 ms</kbq-button-toggle>
                <kbq-button-toggle [value]="2000">2 s</kbq-button-toggle>
            </kbq-button-toggle-group>

            <button kbq-button [disabled]="loading()" (click)="load()">Refresh</button>
        </div>

        <div class="example__card">
            <div class="kbq-text-normal">Updates</div>
            <div class="kbq-display-compact">{{ updates() }}</div>
            @if (showOverlay()) {
                <kbq-loader-overlay size="compact" text="Updating..." />
            }
        </div>
    `,
    styles: `
        :host {
            display: flex;
            flex-direction: column;
            gap: var(--kbq-size-l);
            padding: var(--kbq-size-l);
        }

        .example__controls {
            display: flex;
            flex-wrap: wrap;
            align-items: center;
            gap: var(--kbq-size-l);
        }

        .example__card {
            padding: var(--kbq-size-l);
            border: 1px solid var(--kbq-line-contrast-less);
            border-radius: var(--kbq-size-m);
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayDelayExample {
    private readonly destroyRef = inject(DestroyRef);
    protected readonly delay = model(true);
    protected readonly duration = model(200);
    protected readonly updates = signal(0);
    protected readonly loading = signal(false);
    private readonly loading$ = toObservable(this.loading);
    protected readonly showOverlay = toSignal(
        toObservable(this.delay).pipe(
            switchMap((delay) => (delay ? this.loading$.pipe(kbqDelayLoadingIndicator()) : this.loading$))
        ),
        { initialValue: false }
    );

    protected load(): void {
        this.loading.set(true);

        timer(this.duration())
            .pipe(takeUntilDestroyed(this.destroyRef))
            .subscribe(() => {
                this.updates.update((value) => value + 1);
                this.loading.set(false);
            });
    }
}
```

### Design and animation

#### Color and transparency

If an overlay covers only part of the screen, its edge remains visible. The transition looks seamless when the overlay color matches the container surface. Use the `surface` attribute to select the appearance.

If `surface` is not set, the deprecated `transparent` and `card` attributes select a surface: `transparent="true"` corresponds to `surface="bg"`, `transparent="false"` to `surface="solid"`, and `card="true"` to `surface="card"`. The `card` attribute takes precedence over `transparent`.

| `surface` value | Description                              |
| --------------- | ---------------------------------------- |
| `bg`            | Primary background                       |
| `bg-secondary`  | Secondary background                     |
| `bg-tertiary`   | Tertiary background                      |
| `card`          | Dropdown menus, modal windows, and cards |
| `solid`         | Opaque backdrop                          |

**Example: Loader-overlay background** (`loader-overlay-background`)

```ts loader-overlay-background-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqLoaderOverlayModule, KbqLoaderOverlaySurface } from '@koobiq/components/loader-overlay';

@Component({
    selector: 'loader-overlay-background-example',
    imports: [KbqLoaderOverlayModule],
    template: `
        <div class="example__grid">
            @for (surface of surfaces; track surface.value) {
                <div class="example__item {{ surface.surfaceClass }}">
                    <div class="example__content">
                        <p class="example__paragraph kbq-text-normal">{{ content }}</p>
                        <p class="example__paragraph example__paragraph_fade kbq-text-normal">
                            {{ contentFade }}
                        </p>
                    </div>
                    <kbq-loader-overlay
                        class="example__overlay"
                        size="compact"
                        [text]="surface.value"
                        [surface]="surface.value"
                    />
                </div>
            }
        </div>
    `,
    styles: `
        :host {
            display: block;

            --example-padding: var(--kbq-size-l);
            --example-overlay-transition-duration: 200ms;
            --example-overlay-transition-easing: cubic-bezier(0.23, 1, 0.32, 1);
        }

        .example__grid {
            display: grid;
            grid-template-columns: repeat(4, minmax(0, 1fr));
            gap: 20px;
        }

        .example__item {
            --example-overlay-size-reduction: 0px;
            --example-overlay-inset-y: 0px;
            --example-overlay-inset-x: 0px;

            display: flex;
            flex-direction: column;
            min-width: 0;
            aspect-ratio: 16 / 9;
            box-sizing: border-box;
            padding: var(--example-padding) var(--example-padding) 0;
            border-radius: var(--kbq-size-m);
            grid-column: span 2;
        }

        .example__item:nth-child(5) {
            grid-column: 2 / span 2;
        }

        .example__overlay {
            inset: var(--example-overlay-inset-y) auto auto var(--example-overlay-inset-x);
            width: calc(100% - var(--example-overlay-size-reduction));
            height: calc(100% - var(--example-overlay-size-reduction));
            box-sizing: border-box;
            transition:
                top var(--example-overlay-transition-duration) var(--example-overlay-transition-easing),
                left var(--example-overlay-transition-duration) var(--example-overlay-transition-easing),
                width var(--example-overlay-transition-duration) var(--example-overlay-transition-easing),
                height var(--example-overlay-transition-duration) var(--example-overlay-transition-easing);
        }

        @media (hover: hover) and (pointer: fine) {
            .example__item:hover {
                --example-overlay-size-reduction: var(--example-padding);
                --example-overlay-inset-y: calc(var(--example-padding) * -1);
                --example-overlay-inset-x: calc(var(--example-padding) * 2);
            }
        }

        @media (prefers-reduced-motion: reduce) {
            .example__overlay {
                transition: none;
            }
        }

        .example__content {
            flex: 1;
            overflow: hidden;
        }

        .example__paragraph {
            margin: 0;
        }

        .example__paragraph + .example__paragraph {
            margin-top: 4px;
        }

        .example__paragraph_fade {
            color: var(--kbq-foreground-contrast-secondary);
        }

        .example__item_bg {
            background: var(--kbq-background-bg);
        }

        .example__item_bg-secondary {
            background: var(--kbq-background-bg-secondary);
        }

        .example__item_bg-tertiary {
            background: var(--kbq-background-bg-tertiary);
        }

        .example__item_card {
            background: var(--kbq-background-card);
        }

        @media (max-width: 600px) {
            .example__grid {
                grid-template-columns: 1fr;
            }

            .example__item,
            .example__item:nth-child(5) {
                grid-column: auto;
            }
        }
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayBackgroundExample {
    protected readonly content =
        'Content behind the overlay becomes barely visible, as if hidden in fog. The interface remains recognizable while loading continues, but the main actions are temporarily unavailable.';
    protected readonly contentFade =
        'The overlay blends seamlessly with the background, leaving no visible boundary around its edges. Matching the overlay color to the surface keeps the container connected to the surrounding interface.';

    protected readonly surfaces: ReadonlyArray<{
        value: KbqLoaderOverlaySurface;
        surfaceClass: string;
    }> = [
        { value: 'bg', surfaceClass: 'example__item_bg' },
        { value: 'bg-secondary', surfaceClass: 'example__item_bg-secondary' },
        { value: 'bg-tertiary', surfaceClass: 'example__item_bg-tertiary' },
        { value: 'card', surfaceClass: 'example__item_card' },
        { value: 'solid', surfaceClass: 'example__item_bg' }
    ];
}
```

#### Horizontal alignment and width

The overlay covers the full width and height of the container it is displayed over.
Padding from the overlay edges is 10% of its width.
Maximum width of the text and caption under the spinner is 480px.

#### Vertical alignment

Center alignment with optical compensation is available: the top offset should be smaller than the bottom. By default, the compensation height equals 40% of the block height comprising the loading indicator and captions.

Fixed top-offset alignment is also available (72px by default).

**Example: Loader-overlay fixed-top** (`loader-overlay-fixed-top`)

```ts loader-overlay-fixed-top-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';

@Component({
    selector: 'loader-overlay-fixed-top-example',
    imports: [KbqLoaderOverlayModule, KbqProgressSpinnerModule],
    template: `
        <div class="flex" style="height: 320px">
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text
            <kbq-loader-overlay fixed-top>
                <kbq-progress-spinner kbq-loader-overlay-indicator size="big" [mode]="'indeterminate'" />

                <div kbq-loader-overlay-text>Создание отчета</div>
                <div kbq-loader-overlay-caption>18,7 МБ из 25 МБ — осталось 2 мин</div>
            </kbq-loader-overlay>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayFixedTopExample {}
```

#### Enlarged spinner

When it is necessary to display the loading process on the entire page, an enlarged spinner is used. Do not use single-word "Loading" text with an enlarged spinner.

**Example: Loader-overlay large** (`loader-overlay-large`)

```ts loader-overlay-large-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';
import { KbqProgressSpinnerModule } from '@koobiq/components/progress-spinner';

@Component({
    selector: 'loader-overlay-large-example',
    imports: [KbqLoaderOverlayModule, KbqProgressSpinnerModule],
    template: `
        <div class="flex" style="height: 320px">
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text
            <kbq-loader-overlay [text]="'Загрузка данных'">
                <kbq-progress-spinner size="big" kbq-loader-overlay-indicator [mode]="'indeterminate'" />
            </kbq-loader-overlay>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlayLargeExample {}
```

#### Sizes

The component size is set using the `size` attribute.

**Example: Loader-overlay size** (`loader-overlay-size`)

```ts loader-overlay-size-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { KbqDefaultSizes } from '@koobiq/components/core';
import { KbqLoaderOverlayModule } from '@koobiq/components/loader-overlay';

@Component({
    selector: 'loader-overlay-size-example',
    imports: [KbqLoaderOverlayModule, KbqButtonToggleModule, FormsModule],
    template: `
        <div class="layout-margin-bottom-l">
            <kbq-button-toggle-group [(ngModel)]="size">
                <kbq-button-toggle value="compact">Compact</kbq-button-toggle>
                <kbq-button-toggle value="normal">Normal</kbq-button-toggle>
                <kbq-button-toggle value="big">Big</kbq-button-toggle>
            </kbq-button-toggle-group>
        </div>

        <div class="flex" style="height: 320px">
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text text text text text text text
            text text text text text text text text text text text text text text text
            <kbq-loader-overlay text="Loading data..." [size]="size()" />
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class LoaderOverlaySizeExample {
    readonly size = model<KbqDefaultSizes>('compact');
}
```

## API

### Components

#### KbqLoaderOverlay (component)

Component that covers its parent element while an operation is in progress.

```ts
@Component({ selector: 'kbq-loader-overlay' })
class KbqLoaderOverlay {
    readonly text = input<string>();
    readonly caption = input<string>();
    readonly size = input<KbqDefaultSizes>('big');
    readonly surface = input<KbqLoaderOverlaySurface | null | undefined>();
    /** @deprecated */
    readonly transparent = input<boolean>(true);
    /** @deprecated */
    readonly card = input<boolean>(false);
}
```

- `[text]`: `string` — Text shown under the indicator. Ignored when a `[kbq-loader-overlay-text]` is projected.
- `[caption]`: `string` — Caption shown under the text. Ignored when a `[kbq-loader-overlay-caption]` is projected.
- `[size]`: `KbqDefaultSizes` — Size of the overlay and of its default indicator.
- `[surface]`: `KbqLoaderOverlaySurface | null | undefined` — Sets the surface and opacity mode used by the overlay.

  `solid` is an opaque overlay. Other values select a transparent overlay that matches the corresponding surface.
- `[transparent]`: `boolean` — Controls the legacy overlay opacity when `surface` is not set and `card` is disabled.

  Deprecated: Use `surface="bg"` for a transparent overlay or `surface="solid"` for an opaque overlay. Will
  be removed in 22.0.0.
- `[card]`: `boolean` — Uses a semi-transparent card background and overrides `transparent` when `surface` is not set.

  Deprecated: Use `surface="card"` instead. Will be removed in 22.0.0.

### Directives

#### KbqLoaderOverlayCaption (directive)

Directive that marks a custom caption projected into the overlay.

```ts
@Directive({ selector: '[kbq-loader-overlay-caption]' })
class KbqLoaderOverlayCaption {}
```

#### KbqLoaderOverlayIndicator (directive)

Directive that marks a custom loading indicator projected into the overlay.

```ts
@Directive({ selector: '[kbq-loader-overlay-indicator]' })
class KbqLoaderOverlayIndicator {}
```

#### KbqLoaderOverlayText (directive)

Directive that marks custom text projected into the overlay.

```ts
@Directive({ selector: '[kbq-loader-overlay-text]' })
class KbqLoaderOverlayText {}
```

### Types

#### KbqLoaderOverlaySurface (type)

Semantic background variants for `KbqLoaderOverlay`.

```ts
type KbqLoaderOverlaySurface = 'solid' | 'bg' | 'bg-secondary' | 'bg-tertiary' | 'card';
```
