# Empty state

- Entry point: `@koobiq/components/empty-state`
- NgModule: `KbqEmptyStateModule`
- Page: https://koobiq.io/en/components/empty-state/overview

## Overview

A placeholder component for empty states or error messages. The border is not part of the component; it is shown only for illustration purposes.

**Example: Empty-state default** (`empty-state-overview`)

```ts empty-state-overview-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-overview-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <h2 kbq-empty-state-title>Нет групп</h2>
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
            <div kbq-empty-state-actions>
                <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">
                    <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                    {{ buttonText }}
                </button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateOverviewExample {
    buttonText = 'Создать группу';
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

### Size

For full-screen placeholders, a block with larger-sized elements can be used.

**Example: Empty-state big** (`empty-state-big`)

```ts empty-state-big-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-big-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <kbq-empty-state size="big" style="min-height: 216px">
            <h2 kbq-empty-state-title>Нет групп</h2>
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
            <div kbq-empty-state-actions>
                <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">
                    <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                    {{ buttonText }}
                </button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateBigExample {
    buttonText = 'Создать группу';
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

### Size - variations

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

**Example: Empty-state size** (`empty-state-size`)

```ts empty-state-size-example.ts
import { ChangeDetectionStrategy, Component, model } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqButtonToggleModule } from '@koobiq/components/button-toggle';
import { KbqDefaultSizes } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-size-example',
    imports: [
        FormsModule,
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule,
        KbqButtonToggleModule
    ],
    template: `
        <div>
            <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>

        <kbq-empty-state style="min-height: 216px" [size]="size()">
            <h2 kbq-empty-state-title>No Groups</h2>
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
            <div kbq-empty-state-actions>
                <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">
                    <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                    {{ buttonText }}
                </button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateSizeExample {
    buttonText = 'Create group';
    emptyStateText = 'Agents can be grouped together and assigned the same policies.';
    readonly size = model<KbqDefaultSizes>('compact');
}
```

### Align

Content can be aligned to the center or to the top. The `alignTop` attribute selects the top.

**Example: Empty-state align** (`empty-state-align`)

```ts empty-state-align-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-align-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <div class="layout-row layout-wrap">
            <kbq-empty-state class="flex" style="min-height: 216px">
                <h2 kbq-empty-state-title>Нет групп</h2>
                <div kbq-empty-state-text>{{ emptyStateText }}</div>
                <div kbq-empty-state-actions>
                    <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">
                        <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                        {{ buttonText }}
                    </button>
                </div>
            </kbq-empty-state>

            <kbq-empty-state class="flex" style="min-height: 216px" [alignTop]="true">
                <h2 kbq-empty-state-title>Нет групп</h2>
                <div kbq-empty-state-text>{{ emptyStateText }}</div>
                <div kbq-empty-state-actions>
                    <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">
                        <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                        {{ buttonText }}
                    </button>
                </div>
            </kbq-empty-state>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateAlignExample {
    readonly colors = KbqComponentColors;
    readonly styles = KbqButtonStyles;

    buttonText = 'Создать группу';
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

The component has no height of its own: it grows as a flex item, and in any other container it is exactly as tall as its content, which leaves both alignments looking the same. Give the component or its container a height — the examples here use `min-height` — for the alignment to have an effect.

### Error

The Empty State component can be used to display an error message. It is switched on with the `errorColor` attribute: the title and the text turn red, and an icon in the illustration slot is tinted with them.

**Example: Empty-state error** (`empty-state-error`)

```ts empty-state-error-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule } from '@koobiq/components/button';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-error-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px" [errorColor]="true">
            <i
                kbq-empty-state-icon
                kbq-icon-item="kbq-triangle-exclamation_16"
                [big]="true"
                [color]="'contrast'"
                [fade]="true"
            ></i>
            <h2 kbq-empty-state-title>Не удалось показать записи</h2>
            <div kbq-empty-state-text>
                {{ emptyStateText }}
            </div>
            <div kbq-empty-state-actions>
                <button kbq-button [color]="'theme'" [kbqStyle]="'transparent'">Обновить</button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateErrorExample {
    emptyStateText = 'Проблема с сетью или подключением к БД. Попробуйте еще раз или обратитесь к администратору.';
}
```

### Content

#### Illustration

The illustration size can be changed. A large illustration is suitable for full-screen placeholders. If a placeholder is needed for just a specific block, the illustration can be made compact or omitted entirely.

**Example: Empty-state content** (`empty-state-content`)

```ts empty-state-content-example.ts
import { ChangeDetectionStrategy, Component, computed, inject } from '@angular/core';
import { KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
import { KbqComponentColors, KbqThemeService } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-content-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule,
        KbqIconModule
    ],
    template: `
        <div class="layout-row layout-wrap">
            <kbq-empty-state class="flex" size="big" style="min-height: 216px">
                <div kbq-empty-state-icon>
                    <img
                        src="https://koobiq.io/assets/images/{{ currentTheme() }}/empty_192.webp"
                        alt="Empty state"
                        height="192"
                        width="192"
                        [srcset]="srcSet()"
                    />
                </div>
                <h2 kbq-empty-state-title>Нет групп</h2>
                <div kbq-empty-state-text>{{ emptyStateText }}</div>
                <div kbq-empty-state-actions>
                    <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">
                        <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                        {{ buttonText }}
                    </button>
                </div>
            </kbq-empty-state>

            <kbq-empty-state class="flex" style="min-height: 216px">
                <div kbq-empty-state-icon>
                    <img
                        src="https://koobiq.io/assets/images/{{ currentTheme() }}/empty_192.webp"
                        alt="Empty state"
                        width="80"
                        height="80"
                        [srcset]="srcSet()"
                    />
                </div>
                <h2 kbq-empty-state-title>Нет групп</h2>
                <div kbq-empty-state-text>{{ emptyStateText }}</div>
                <div kbq-empty-state-actions>
                    <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">
                        <i kbq-icon="kbq-plus_16" [color]="'theme'"></i>
                        {{ buttonText }}
                    </button>
                </div>
            </kbq-empty-state>
        </div>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateContentExample {
    readonly colors = KbqComponentColors;
    readonly styles = KbqButtonStyles;
    private readonly themeService = inject(KbqThemeService, { optional: true });
    protected readonly currentTheme = computed(() => this.themeService?.colorScheme() ?? 'light');

    protected readonly srcSet = computed(() => {
        const currentTheme = this.currentTheme();

        return `https://koobiq.io/assets/images/${currentTheme}/empty_192.webp 1x, https://koobiq.io/assets/images/${currentTheme}/empty_192@2x.webp 2x`;
    });

    buttonText = 'Создать группу';
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

In a center-aligned component with an illustration, an additional bottom offset is added, which raises the composition to provide optical compensation.

#### Icon

An icon can be used as the illustration.

**Example: Empty-state icon** (`empty-state-icon`)

```ts empty-state-icon-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';
import { KbqIconModule } from '@koobiq/components/icon';

@Component({
    selector: 'empty-state-icon-example',
    imports: [
        KbqEmptyStateModule,
        KbqIconModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <div kbq-empty-state-icon>
                <i kbq-icon-item="kbq-circle-info_16" [big]="true" [color]="'contrast'" [fade]="true"></i>
            </div>
            <div kbq-empty-state-text>
                This should detail the actions you can take on this screen, as well as why it’s valuable.
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateIconExample {}
```

#### Title

**Example: Empty-state title** (`empty-state-title`)

```ts empty-state-title-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';

@Component({
    selector: 'empty-state-title-example',
    imports: [
        KbqEmptyStateModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <h2 kbq-empty-state-title>Нет групп</h2>
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateTitleExample {
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

#### Actions

One or more actions can be placed below the text: buttons, links, and pseudo-links.

**Example: Empty-state actions** (`empty-state-actions`)

```ts empty-state-actions-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';

@Component({
    selector: 'empty-state-actions-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
            <div class="layout-row layout-align-center" kbq-empty-state-actions style="min-width: 340px">
                <button class="layout-margin-right-s" kbq-button [color]="colors.Contrast" [kbqStyle]="styles.Filled">
                    {{ buttonText }}
                </button>

                <button kbq-button [color]="colors.ContrastFade" [kbqStyle]="styles.Filled">Объединить группы</button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateActionsExample {
    readonly styles = KbqButtonStyles;
    readonly colors = KbqComponentColors;

    buttonText = 'Создать группу';
    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

**Example: Empty-state actions more** (`empty-state-actions2`)

```ts empty-state-actions2-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonModule, KbqButtonStyles } from '@koobiq/components/button';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';

@Component({
    selector: 'empty-state-actions2-example',
    imports: [
        KbqEmptyStateModule,
        KbqButtonModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <div kbq-empty-state-text>{{ emptyStateText }}</div>
            <div kbq-empty-state-actions>
                <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">Action 1</button>
                <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">Action 2</button>
                <button kbq-button [color]="colors.Theme" [kbqStyle]="styles.Transparent">Action 3</button>
            </div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateActions2Example {
    readonly styles = KbqButtonStyles;
    readonly colors = KbqComponentColors;

    emptyStateText = 'Агенты можно объединить в группу и назначить им одни и те же политики';
}
```

#### Text only

**Example: Empty-state text only** (`empty-state-text-only`)

```ts empty-state-text-only-example.ts
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { KbqButtonStyles } from '@koobiq/components/button';
import { KbqComponentColors } from '@koobiq/components/core';
import { KbqEmptyStateModule } from '@koobiq/components/empty-state';

@Component({
    selector: 'empty-state-text-only-example',
    imports: [
        KbqEmptyStateModule
    ],
    template: `
        <kbq-empty-state style="min-height: 216px">
            <div kbq-empty-state-text>Нет информации</div>
        </kbq-empty-state>
    `,
    changeDetection: ChangeDetectionStrategy.OnPush
})
export class EmptyStateTextOnlyExample {
    readonly styles = KbqButtonStyles;
    readonly colors = KbqComponentColors;
}
```

#### Conditional slots

When a slot is rendered conditionally, the condition has to be top level: the `@if` wraps the slot element itself.

```html
@if (failed()) {
<i kbq-empty-state-icon kbq-icon-item="kbq-triangle-exclamation_16"></i>
}
```

Blocks nested in one another, and several elements in one block, do not work — that is how content projection behaves in Angular. Such content never reaches the DOM, with no error and no warning.

When there are several variants, give each its own `@if` at the same level: `@if (hasIcon() && wrapped())` and `@if (hasIcon() && !wrapped())`.

### Accessibility

The title is rendered with heading typography — `subheading` at the normal size, `headline` at the big one — so write it as a real heading of the level the surrounding page calls for, `<h2 kbq-empty-state-title>`, rather than as a `div`. The component zeroes the browser's own heading margin, so the spacing stays the same whichever element is used.

An empty state is usually inserted into a page that has already been read: after a search returns nothing, or after a request fails. Add `role="status"` on `<kbq-empty-state>` so that assistive technology announces it, or `role="alert"` for the error variant, which is announced more insistently.

The component adds no `role` of its own, because an empty state that is part of the page from the start — an initially empty list, say — has nothing to announce.

## API

### Components

#### KbqEmptyState (component)

Placeholder rendered in place of content that is empty, unavailable or failed to load. Projects an
illustration, a title, a text and an actions row, in that order.

```html
<kbq-empty-state>
    <h2 kbq-empty-state-title>No groups</h2>
    <div kbq-empty-state-text>Agents can be grouped and given the same policies</div>
</kbq-empty-state>
```

The host has no intrinsic height — it grows as a flex item and is otherwise as tall as its
content, which leaves `alignTop` indistinguishable from the default. Give the host or its
container a height for the alignment to mean anything.

For an empty state inserted dynamically, add `role="status"` — or `role="alert"` for the error
variant — on the host so assistive technology announces it. See the component guide.

```ts
@Component({ selector: 'kbq-empty-state', exportAs: 'kbqEmptyState' })
class KbqEmptyState implements KbqEmptyStateContext {
    readonly errorColor = input<boolean>(false);
    readonly alignTop = input<boolean>(false);
    readonly size = input<KbqDefaultSizes>('normal');
}
```

- `[errorColor]`: `boolean` — Whether the title, the text and the projected icon render in the error color. Defaults to `false`.
- `[alignTop]`: `boolean` — Whether the content sits at the top of the host instead of its center. Defaults to `false`.
- `[size]`: `KbqDefaultSizes` — Size tier of the placeholder: `compact`, `normal` or `big`. Drives every padding and offset,
  the maximum width of the title and the text, and their typography. Defaults to `normal`.

### Directives

#### KbqEmptyStateActions (directive)

Actions slot of `kbq-empty-state`, rendered below the text. Holds buttons, links or pseudo-links.

```ts
@Directive({ selector: '[kbq-empty-state-actions]' })
class KbqEmptyStateActions {}
```

#### KbqEmptyStateIcon (directive)

Illustration slot of `kbq-empty-state`. Holds an icon, an image or any other element.

A `kbq-icon-item` follows the empty state's `errorColor`, whether the directive sits on the icon
itself or wraps it.

```ts
@Directive({ selector: '[kbq-empty-state-icon]' })
class KbqEmptyStateIcon {}
```

#### KbqEmptyStateText (directive)

Text slot of `kbq-empty-state`, rendered below the title.

```ts
@Directive({ selector: '[kbq-empty-state-text]' })
class KbqEmptyStateText {}
```

#### KbqEmptyStateTitle (directive)

Title slot of `kbq-empty-state`, rendered below the illustration.

It carries heading typography — `subheading` at `size="normal"`, `headline` at `size="big"` — so
the host element should be a real heading of the level the surrounding page calls for:
`<h2 kbq-empty-state-title>`.

```ts
@Directive({ selector: '[kbq-empty-state-title]' })
class KbqEmptyStateTitle {}
```

### Types

#### KbqEmptyStateContext (interface)

The part of `kbq-empty-state` its projected slots follow.

```ts
interface KbqEmptyStateContext {
    readonly errorColor: Signal<boolean>;
}
```

- `errorColor`: `Signal<boolean>` — Whether the empty state renders in the error color.
