` ).
### Example
```html
@for (digit of digits; track digit) { {{ digit }} } C
0
Value: {{ value() || '—' }}
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [columns] | `number` | Number of grid columns |
### Usage Examples
#### Basic keypad
A numeric pad — digits, clear and backspace wired via `(click)` .
**Template:**
```html
@for (digit of digits; track digit) { {{ digit }} } C
0
Value: {{ value() || '—' }}
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiKeypad} from '@taiga-ui/addon-mobile';
import {TuiIcon} from '@taiga-ui/core';
@Component({
imports: [TuiIcon, TuiKeypad],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly digits = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
protected readonly value = signal('');
protected append(digit: string): void {
this.value.update((current) => `${current}${digit}`);
}
protected backspace(): void {
this.value.update((current) => current.slice(0, -1));
}
protected clear(): void {
this.value.set('');
}
}
```
**LESS:**
```less
.keypad {
inline-size: 15rem;
}
```
#### Custom keys
A custom-font pad with a conditional backspace icon, driving a display.
**Template:**
```html
{{ value() || '—' }}
@for (digit of digits; track digit) { {{ digit }} }
0 @if (value()) {
}
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiKeypad} from '@taiga-ui/addon-mobile';
import {TuiIcon} from '@taiga-ui/core';
@Component({
imports: [TuiIcon, TuiKeypad],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly digits = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
protected readonly value = signal('');
protected append(digit: string): void {
this.value.update((current) => `${current}${digit}`);
}
protected backspace(): void {
this.value.update((current) => current.slice(0, -1));
}
protected clear(): void {
this.value.set('');
}
}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
}
.custom {
inline-size: 15rem;
font:
normal 2.5rem/1 'SF Pro Display',
-apple-system,
system-ui;
}
.display {
font: bold 2.5rem/1 var(--tui-typography-family-display);
text-align: center;
color: var(--tui-text-primary);
inline-size: 100%;
}
```
#### Shuffled PIN
Digit positions are randomized at runtime — the arbitrary, dynamic content a PIN terminal needs.
**Template:**
```html
{{ masked() || '—' }}
@for (digit of digits(); track digit) { {{ digit }} } C
```
**TypeScript:**
```ts
import {Component, computed, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiKeypad} from '@taiga-ui/addon-mobile';
import {TuiIcon} from '@taiga-ui/core';
const PIN_LENGTH = 4;
const DIGITS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9'];
@Component({
imports: [TuiIcon, TuiKeypad],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
// Positions are randomized on every clear — like PIN terminals that reshuffle the pad
protected readonly digits = signal(this.shuffle());
protected readonly pin = signal('');
protected readonly masked = computed(() => '•'.repeat(this.pin().length));
protected append(digit: string): void {
if (this.pin().length < PIN_LENGTH) {
this.pin.update((current) => `${current}${digit}`);
}
}
protected backspace(): void {
this.pin.update((current) => current.slice(0, -1));
}
protected clear(): void {
this.pin.set('');
this.digits.set(this.shuffle());
}
private shuffle(): string[] {
// uniform shuffle: independent random key per digit, then sort by it
return DIGITS.map((digit) => [Math.random(), digit] as const)
.sort(([a], [b]) => a - b)
.map(([, digit]) => digit);
}
}
```
**LESS:**
```less
.pin {
margin: 0;
font: bold 2.5rem/1.4 var(--tui-typography-family-display);
letter-spacing: 0.5rem;
text-align: center;
color: var(--tui-text-primary);
}
.keypad {
inline-size: 15rem;
}
```
#### Collapsible
The keypad expands while the amount field is focused, inside a sheet.
**Template:**
```html
Open form
Send
Commission-free
@if (focused()) {
@for (digit of digits; track digit) { {{ digit }} }
0
}
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {
TuiKeypad,
TuiSheetDialog,
type TuiSheetDialogOptions,
} from '@taiga-ui/addon-mobile';
import {TuiAnimated} from '@taiga-ui/cdk';
import {TuiButton, TuiCell, TuiIcon, TuiTitle} from '@taiga-ui/core';
import {TuiAvatar, TuiChip} from '@taiga-ui/kit';
import {TuiFloatingContainer} from '@taiga-ui/layout';
@Component({
imports: [
FormsModule,
TuiAnimated,
TuiAvatar,
TuiButton,
TuiCell,
TuiChip,
TuiFloatingContainer,
TuiIcon,
TuiKeypad,
TuiSheetDialog,
TuiTitle,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly digits = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
protected readonly value = signal('143');
protected readonly open = signal(false);
protected readonly focused = signal(false);
protected readonly options: Partial
= {
closable: true,
bar: false,
appearance: 'fullscreen',
};
protected append(digit: string): void {
this.value.update((current) => `${current}${digit}`);
}
protected backspace(): void {
this.value.update((current) => current.slice(0, -1));
}
protected clear(): void {
this.value.set('');
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils.less';
.sheet {
display: flex;
flex-direction: column;
flex-grow: 1;
min-block-size: 100%;
}
.top {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
padding: 2rem 1rem 1rem;
}
.amount {
box-sizing: border-box;
padding: 1rem;
font: bold 2.5rem var(--tui-typography-family-display);
text-align: center;
color: var(--tui-text-primary);
// auto width via [size] so a centered never scrolls/clips its text on iOS Safari
max-inline-size: 100%;
border: none;
outline: none;
background: transparent;
transform: translateZ(0);
}
.chips {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 0.5rem;
}
.middle {
display: flex;
flex: 1;
flex-direction: column;
justify-content: flex-end;
padding-block-end: 1rem;
}
.card {
background: var(--tui-background-neutral-1);
border-radius: var(--tui-radius-l);
overflow: hidden;
}
.clamp {
.tui-line-clamp();
margin-block-start: 0.5rem;
min-block-size: 2rem;
&.tui-enter,
&.tui-leave {
animation-name: tuiFade;
}
}
.pad {
display: grid;
grid-area: auto / 1;
// clip only while collapsing/expanding, so pressed keys aren't cut off at rest
&.tui-enter .pad-inner,
&.tui-leave .pad-inner {
overflow: hidden;
}
&.tui-enter,
&.tui-leave {
animation-name: tuiFade, tuiCollapse;
}
}
```
#### Calculator
A calculator in a picture-in-picture window — icon keys and hardware-keyboard mapping.
**Template:**
```html
Open Calculator
AC
(
)
@for (digit of ['7', '8', '9']; track digit) { {{ digit }} }
@for (digit of ['4', '5', '6']; track digit) { {{ digit }} }
@for (digit of ['1', '2', '3']; track digit) { {{ digit }} }
0
.
```
**TypeScript:**
```ts
import {Component, computed, type ElementRef, signal, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiKeypad} from '@taiga-ui/addon-mobile';
import {TuiAutoFocus} from '@taiga-ui/cdk';
import {TuiButton, TuiIcon} from '@taiga-ui/core';
import {TuiPopout} from '@taiga-ui/experimental';
const KEYBOARD_MAP: Record = {
'+': '+',
'-': '-',
'*': '×',
'/': '÷',
'(': '(',
')': ')',
'.': '.',
Enter: 'enter',
Backspace: 'backspace',
Escape: 'clear',
};
const OPERATORS = new Set(['-', '+', '÷', '×']);
@Component({
imports: [TuiAutoFocus, TuiButton, TuiIcon, TuiKeypad, TuiPopout],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly input = viewChild>('input');
protected readonly expression = signal('0');
protected readonly displayValue = computed(() => this.expression());
protected readonly open = signal(false);
protected onKey(key: string): void {
const el = this.input()?.nativeElement;
el?.focus();
this.expression.update((expr) => this.reduce(expr, key));
// a readonly, programmatically-set field won't scroll itself — after layout, keep
// the latest input (the tail) in view
requestAnimationFrame(() => {
if (el) {
el.scrollLeft = el.scrollWidth;
}
});
}
protected onKeydown(event: KeyboardEvent): void {
const mapped =
KEYBOARD_MAP[event.key] ?? (/^\d$/.test(event.key) ? event.key : null);
if (mapped) {
event.preventDefault();
this.onKey(mapped);
}
}
private reduce(current: string, key: string): string {
const expr = current === 'Error' ? '0' : current;
switch (key) {
case '.':
return this.appendDot(expr);
case '(':
return expr === '0' ? '(' : `${expr}(`;
case ')':
return `${expr})`;
case 'backspace':
return expr.length > 1 ? expr.replace(/\s?.$/, '') || '0' : '0';
case 'clear':
return '0';
case 'enter':
return this.tryCalculate(expr);
default:
return OPERATORS.has(key)
? this.appendOperator(expr, key)
: this.appendDigit(expr, key);
}
}
private appendDigit(expr: string, digit: string): string {
return expr.replace(/\d*(?:\.\d*)?$/, (last) =>
last === '0' ? digit : `${last}${digit}`,
);
}
private appendOperator(expr: string, op: string): string {
return /[+\-×÷]\s*$/.test(expr)
? expr.replace(/[+\-×÷]\s*$/, `${op} `)
: `${expr} ${op} `;
}
private appendDot(expr: string): string {
const lastNumber = expr.split(/[-+*/×÷()\s]/).pop() ?? '';
if (lastNumber.includes('.')) {
return expr;
}
return lastNumber ? `${expr}.` : `${expr}0.`;
}
private tryCalculate(expr: string): string {
try {
return this.calculate(expr);
} catch {
return 'Error';
}
}
private calculate(expr: string): string {
const tokens = expr
.replaceAll('×', '*')
.replaceAll('÷', '/')
.match(/\d+(?:\.\d+)?|[+\-*/()]/g);
if (!tokens) {
throw new Error('Empty expression');
}
const result = this.evaluate(tokens);
if (!Number.isFinite(result)) {
throw new Error('Not a finite number');
}
return String(Math.round(result * 1e10) / 1e10);
}
/**
* Safe evaluation of a flat arithmetic expression via the shunting-yard
* algorithm — no `eval`/`new Function`, only the four operators and parentheses.
*/
private evaluate(tokens: readonly string[]): number {
const values: number[] = [];
const operators: string[] = [];
const fold = (): void => {
const operator = operators.pop();
const right = values.pop();
const left = values.pop();
if (operator === undefined || left === undefined || right === undefined) {
throw new Error('Malformed expression');
}
values.push(this.operate(left, right, operator));
};
for (const token of tokens) {
if (/\d/.test(token)) {
values.push(Number(token));
} else if (token === '(') {
operators.push(token);
} else if (token === ')') {
while (operators.length && operators[operators.length - 1] !== '(') {
fold();
}
if (operators.pop() !== '(') {
throw new Error('Unbalanced parentheses');
}
} else {
while (
operators.length &&
this.precedence(operators[operators.length - 1]) >=
this.precedence(token)
) {
fold();
}
operators.push(token);
}
}
while (operators.length) {
if (operators[operators.length - 1] === '(') {
throw new Error('Unbalanced parentheses');
}
fold();
}
const [result, ...rest] = values;
if (result === undefined || rest.length) {
throw new Error('Malformed expression');
}
return result;
}
private precedence(operator: string | undefined): number {
if (operator === '+' || operator === '-') {
return 1;
}
return operator === '*' || operator === '/' ? 2 : 0;
}
private operate(left: number, right: number, operator: string): number {
if (operator === '-') {
return left - right;
}
if (operator === '+') {
return left + right;
}
if (operator === '*') {
return left * right;
}
return right === 0 ? Number.NaN : left / right;
}
}
```
**LESS:**
```less
:host {
display: block;
block-size: 100%;
}
.calculator {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
border-radius: 0.5rem;
block-size: 100dvh;
box-sizing: border-box;
}
tui-keypad {
flex: 1;
min-block-size: 0;
grid-auto-rows: 1fr; // fill the flex height instead of the fixed default rows
}
.input {
flex: none;
inline-size: 100%;
padding: 0.75rem 1rem;
font-size: 1.5rem;
font-weight: 500;
text-align: end;
border: none;
outline: none;
background: var(--tui-background-neutral-2);
border-radius: 0.5rem;
color: var(--tui-text-primary);
box-sizing: border-box;
&::placeholder {
color: var(--tui-text-secondary);
}
}
```
### TypeScript
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiKeypad} from '@taiga-ui/addon-mobile';
import {TuiIcon} from '@taiga-ui/core';
@Component({
imports: [TuiDemo, TuiIcon, TuiKeypad],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected readonly examples = [
'Basic keypad',
'Custom keys',
'Shuffled PIN',
'Collapsible',
'Calculator',
];
protected readonly digits = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
protected columns = 3;
protected readonly columnsVariants = [3, 4];
protected readonly value = signal('');
protected append(digit: string): void {
this.value.update((current) => `${current}${digit}`);
}
protected backspace(): void {
this.value.update((current) => current.slice(0, -1));
}
protected clear(): void {
this.value.set('');
}
}
```
### LESS
```less
tui-keypad {
inline-size: 15rem;
// height follows width via the pad's own aspect-ratio, so the cells stay square for
// any [columns] value — a fixed height would squash them at columns=4
block-size: auto;
}
```
---
# components/Label
- **Package**: `CORE`
- **Type**: components
- **Version**: 5.0.0
Label is used to show text related to textfields, checkboxes, toggles and radio buttons
### Usage Examples
#### Basic
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiCheckbox, TuiIcon, TuiLabel, TuiTitle} from '@taiga-ui/core';
import {TuiTooltip} from '@taiga-ui/kit';
@Component({
imports: [ReactiveFormsModule, TuiCheckbox, TuiIcon, TuiLabel, TuiTitle, TuiTooltip],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected form = new FormGroup({
testValue1: new FormControl(true),
testValue2: new FormControl(false),
testValue3: new FormControl(false),
});
}
```
#### Small size
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLabel, TuiRadio} from '@taiga-ui/core';
@Component({
imports: [ReactiveFormsModule, TuiLabel, TuiRadio],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected form = new FormGroup({value: new FormControl()});
}
```
#### Switch
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLabel} from '@taiga-ui/core';
import {TuiSwitch} from '@taiga-ui/kit';
@Component({
imports: [ReactiveFormsModule, TuiLabel, TuiSwitch],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected form = new FormGroup({
testValue1: new FormControl(true),
testValue2: new FormControl(false),
testValue3: new FormControl(false),
});
}
```
#### Textfield
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiInput} from '@taiga-ui/core';
@Component({
imports: [ReactiveFormsModule, TuiInput],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected form = new FormGroup({value: new FormControl('Input value')});
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = ['Basic', 'Small size', 'Switch', 'Textfield'];
}
```
---
# components/LegendItem
- **Package**: `ADDON-CHARTS`
- **Type**: components
- **Version**: 5.0.0
A button for a legend of ring or pie charts
### Example
```html
{{ 123456 | tuiAmount: 'RUB' }}
tuiAmount pipe is used to format currency and fraction
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [active] | `boolean` | active state from outside |
| [color] | `TuiColor \| string \| null` | indicator color |
| [disabled] | `boolean` | disabled item (i.e. hidden from the related chart) |
| [size] | `TuiSizeS` | — |
| [value] | `string` | text inside |
### Usage Examples
#### With a ring chart
**Template:**
```html
{{ sum | tuiAmount: 'RUB' }}
Total
@for (label of labels; track label) {
{{ value[$index] || 0 | tuiAmount: 'RUB' }}
}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLegendItem, TuiRingChart} from '@taiga-ui/addon-charts';
import {TuiAmountPipe} from '@taiga-ui/addon-commerce';
import {TuiHovered, tuiSum} from '@taiga-ui/cdk';
@Component({
imports: [TuiAmountPipe, TuiHovered, TuiLegendItem, TuiRingChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected activeItemIndex = Number.NaN;
protected readonly value = [13769, 12367, 10172, 3018, 2592];
protected readonly sum = tuiSum(...this.value);
protected readonly labels = ['Food', 'Cafe', 'OSS', 'Taxi', 'Other'];
protected isItemActive(index: number): boolean {
return this.activeItemIndex === index;
}
protected onHover(index: number, hovered: boolean): void {
this.activeItemIndex = hovered ? index : Number.NaN;
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
--tui-chart-categorical-00: #c779d0;
--tui-chart-categorical-01: #feac5e;
--tui-chart-categorical-02: #ff5f6d;
--tui-chart-categorical-03: #4bc0c8;
--tui-chart-categorical-04: #9795cd;
}
.wrapper {
display: flex;
align-items: center;
@media @tui-mobile {
flex-direction: column;
}
}
.legend {
margin: 0 0 0 2rem;
@media @tui-mobile {
margin: 2rem 0 0;
}
}
.item {
margin: 0 0.5rem 0.75rem 0;
}
```
#### Toggling
**Template:**
```html
In case you need to be able to toggle a category by separate action, for example, if clicking on it should expand it for more details
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLegendItem, TuiRingChart} from '@taiga-ui/addon-charts';
import {TuiAmountPipe} from '@taiga-ui/addon-commerce';
import {tuiSum} from '@taiga-ui/cdk';
import {
TuiCheckbox,
TuiIcon,
TuiNotification,
TuiNotificationService,
} from '@taiga-ui/core';
import {tuiFormatNumber} from '@taiga-ui/kit';
@Component({
imports: [
TuiAmountPipe,
TuiCheckbox,
TuiIcon,
TuiLegendItem,
TuiNotification,
TuiRingChart,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly alerts = inject(TuiNotificationService);
private enabled = Array.from({length: 5}, () => true);
protected readonly data = [13769, 12367, 10172, 3018, 2592];
protected readonly sum = tuiSum(...this.data);
protected readonly labels = ['Axes', 'Faxes', 'Taxes', 'Saxes', 'Other'];
protected get value(): readonly number[] {
return this.data.map((value, index) => (this.enabled[index] ? value : 0));
}
protected isEnabled(index: number): boolean {
return this.enabled[index] ?? false;
}
protected toggle(index: number): void {
this.enabled = this.enabled.map((value, i) => (i === index ? !value : value));
}
protected onClick(index: number): void {
if (this.isEnabled(index)) {
this.alerts
.open(`Category spending: ${tuiFormatNumber(this.data[index] ?? 0)} ₽`, {
label: this.labels[index],
})
.subscribe();
} else {
this.toggle(index);
}
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
--tui-chart-categorical-00: #c779d0;
--tui-chart-categorical-01: #feac5e;
--tui-chart-categorical-02: #ff5f6d;
--tui-chart-categorical-03: #4bc0c8;
--tui-chart-categorical-04: #9795cd;
}
.chart {
pointer-events: none;
}
.wrapper {
display: flex;
align-items: center;
margin-block-start: 1rem;
@media @tui-mobile {
flex-direction: column;
}
}
.disable {
.transition(~'transform, color');
margin-inline-start: 0.5rem;
will-change: transform;
color: var(--tui-text-secondary);
pointer-events: auto;
&::before {
font-size: 1rem;
}
&:hover {
color: var(--tui-text-primary);
}
&_rotated {
transform: rotate(45deg);
}
}
.legend {
margin: 0 0 0 2rem;
justify-content: center;
@media @tui-mobile {
margin: 2rem 0 0;
}
}
.item {
margin: 0 0.5rem 0.75rem 0;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiLegendItem} from '@taiga-ui/addon-charts';
import {TuiAmountPipe} from '@taiga-ui/addon-commerce';
import {type TuiSizeS} from '@taiga-ui/core';
@Component({
imports: [TuiAmountPipe, TuiDemo, TuiLegendItem],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected text = 'Text inside';
protected active = false;
protected readonly examples = ['With a ring chart', 'Toggling'];
protected readonly sizeVariants: readonly TuiSizeS[] = ['s', 'm'];
protected size = this.sizeVariants[0]!;
protected disabled = false;
protected color = '';
protected readonly colorVariants: readonly string[] = [
'var(--tui-chart-categorical-04)',
'var(--tui-background-accent-1)',
'var(--tui-background-neutral-1)',
];
}
```
---
# components/Like
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
A like component based on native checkbox with icons and custom color for icon when `:checked` state.
### Usage Examples
#### Basic
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLike} from '@taiga-ui/kit';
@Component({
imports: [TuiLike],
templateUrl: './index.html',
styles: ':host { display: flex; gap: 1rem; align-items: center; }',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Icons from DI
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLike, tuiLikeOptionsProvider} from '@taiga-ui/kit';
@Component({
imports: [TuiLike],
templateUrl: './index.html',
styles: ':host { display: flex; gap: 1rem; align-items: center; }',
encapsulation,
changeDetection,
providers: [
tuiLikeOptionsProvider({
icons: {unchecked: '@tui.star', checked: '@tui.star-filled'},
}),
],
})
export default class Example {}
```
#### External icons
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLike, tuiLikeOptionsProvider} from '@taiga-ui/kit';
@Component({
imports: [TuiLike],
templateUrl: './index.html',
styles: ':host { display: flex; gap: 1rem; align-items: center; }',
encapsulation,
changeDetection,
providers: [
tuiLikeOptionsProvider({
icons: {
unchecked:
'https://raw.githubusercontent.com/MarsiBarsi/readme-icons/main/github.svg',
checked: '/assets/icons/github.svg',
},
}),
],
})
export default class Example {}
```
#### Other appearances
**Template:**
```html
@for (appearance of appearances; track appearance) { }
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLike} from '@taiga-ui/kit';
@Component({
imports: [TuiLike],
templateUrl: './index.html',
styles: ':host { display: flex; gap: 1rem; align-items: center; }',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly appearances = ['negative', 'positive', 'warning', 'flat'] as const;
}
```
#### With forms
**Template:**
```html
Reactive form
Liked: {{ likeForm.value.liked }}
Toggle
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormControl, FormGroup, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiLike} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, ReactiveFormsModule, TuiButton, TuiLike],
templateUrl: './index.html',
styles: ':host { display: flex; column-gap: 3rem; justify-content: space-between; flex-wrap: wrap; }',
encapsulation,
changeDetection,
})
export default class Example {
protected liked = false;
protected likeForm = new FormGroup({liked: new FormControl(false)});
protected changeValue(): void {
this.liked = !this.liked;
this.likeForm.setValue({liked: !this.likeForm.value.liked});
}
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = [
'Basic',
'Icons from DI',
'External icons',
'Other appearances',
'With forms',
];
}
```
---
# components/LineChart
- **Package**: `ADDON-CHARTS`
- **Type**: components
- **Version**: 5.0.0
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [dots] | `boolean` | show dots on chart |
| [filled] | `boolean` | filled with gradient |
| [height] | `number` | axis Y range, pixel scale is 1:1 |
| [y] | `number` | start of Y axis |
| [width] | `number` | axis X range, pixel scale is 1:1 |
| [x] | `number` | start of X axis |
| [smoothingFactor] | `number` | smoothing factor from 0 to 99 |
| [value] | `TuiPoint[]` | array of data |
| [xStringify] | `TuiStringHandler \| null` | function to stringify a value number to a string in axis X hint |
| [yStringify] | `TuiStringHandler \| null` | function to stringify a value number to a string in axis Y hint |
| [tuiLineChartHint] | `PolymorpheusContent>` | |
### Usage Examples
#### Line
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAxes, TuiLineChart, TuiLineChartHint} from '@taiga-ui/addon-charts';
import {type TuiContext} from '@taiga-ui/cdk';
import {type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiLineChart, TuiLineChartHint],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value: readonly TuiPoint[] = [
[50, 50],
[100, 75],
[150, 50],
[200, 150],
[250, 155],
[300, 190],
[350, 90],
];
protected readonly stringify = String;
protected readonly hintContent = ({
$implicit,
}: TuiContext): number => $implicit[0]?.[1] ?? 0;
}
```
**LESS:**
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
color: #bc71c9;
}
```
#### Smooth
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAxes, TuiLineChart} from '@taiga-ui/addon-charts';
import {type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiLineChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value: readonly TuiPoint[] = [
[50, 50],
[100, 75],
[150, 50],
[200, 150],
[250, 155],
[300, 190],
[350, 90],
];
}
```
**LESS:**
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
color: #bc71c9;
}
```
#### Dotted
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAxes, TuiLineChart} from '@taiga-ui/addon-charts';
import {type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiLineChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly dotted: readonly TuiPoint[] = [
[50, 50],
[100, 75],
[150, 50],
];
protected readonly solid: readonly TuiPoint[] = [
[150, 50],
[200, 150],
[250, 155],
];
protected readonly dashed: readonly TuiPoint[] = [
[250, 155],
[300, 190],
[350, 90],
];
}
```
**LESS:**
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
color: #bc71c9;
}
.chart {
position: absolute;
inset-block-start: 0;
inset-inline-start: 0;
&_dotted {
stroke-dasharray: 2;
}
&_dashed {
stroke-dasharray: 4;
}
}
```
#### Hint
**Template:**
```html
Vertical: {{ value[0] }}
Horizontal: {{ value[1] }}
index: {{ index }}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAxes, TuiChartHint, TuiLineChart} from '@taiga-ui/addon-charts';
import {type TuiContext, type TuiStringHandler} from '@taiga-ui/cdk';
import {TuiHint, type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiChartHint, TuiHint, TuiLineChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value: TuiPoint[] = [
[50, 50],
[100, 75],
[150, 50],
[200, 150],
[250, 155],
[300, 190],
[350, 90],
];
protected readonly singleValue: TuiPoint[] = [[200, 150]];
protected readonly hint: TuiStringHandler> = ({$implicit}) =>
`Vertical: ${$implicit[1]}\nHorizontal: ${$implicit[0]}`;
}
```
**LESS:**
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
color: #bc71c9;
}
```
#### Several lines with hints
**Template:**
```html
@for (value of values; track value) { }
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAxes, TuiLineChart, TuiLineChartHint} from '@taiga-ui/addon-charts';
import {type TuiContext, type TuiStringHandler} from '@taiga-ui/cdk';
import {type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiLineChart, TuiLineChartHint],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly values: TuiPoint[][] = [
[
[50, 50],
[100, 75],
[150, 50],
[200, 150],
[250, 155],
[300, 190],
[350, 90],
],
[
[50, 40],
[100, 60],
[150, 90],
[200, 120],
[250, 150],
[300, 110],
[350, 130],
],
[
[50, 0],
[100, 0],
[150, 80],
[200, 50],
[250, 130],
[300, 200],
[350, 200],
],
];
protected readonly hint: TuiStringHandler> = ({
$implicit,
}) => `${$implicit[0]?.[0]} items:\n\n${$implicit.map(([_, y]) => y).join('$\n')}$`;
}
```
**LESS:**
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
}
.chart {
position: absolute;
color: #ffb74c;
&:first-child {
color: #bc71c9;
}
&:last-child {
color: #4dc3f7;
}
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {TuiDocHint} from '@demo/components/hint';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiAxes, TuiLineChart, TuiLineChartHint} from '@taiga-ui/addon-charts';
import {type TuiContext, type TuiStringHandler} from '@taiga-ui/cdk';
import {type TuiPoint} from '@taiga-ui/core';
@Component({
imports: [TuiAxes, TuiDemo, TuiDocHint, TuiLineChart, TuiLineChartHint],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected readonly examples = [
'Line',
'Smooth',
'Dotted',
'Hint',
'Several lines with hints',
];
protected readonly value: readonly TuiPoint[] = [
[50, 50],
[100, 75],
[150, 50],
[200, 150],
[250, 155],
[300, 190],
[350, 90],
];
protected readonly yStringifyVariants: ReadonlyArray> = [
(y) => `${(10 * y).toLocaleString('ru-RU', {maximumFractionDigits: 0})} $`,
];
protected readonly xStringifyVariants: ReadonlyArray> = [
(x) => `${100 * x}`,
];
protected readonly hintContentVariants: ReadonlyArray<
TuiStringHandler>
> = [
({$implicit}) =>
$implicit.map(([x, y]) => `Vertical: ${y}\nHorizontal: ${x}`).join('\n'),
];
protected hintContent: TuiStringHandler> | null =
null;
protected yStringify: TuiStringHandler | null = null;
protected xStringify: TuiStringHandler | null = null;
protected x = 0;
protected y = 0;
protected width = 400;
protected height = 200;
protected smoothingFactor = 0;
protected filled = false;
protected dots = false;
}
```
### LESS
```less
.axes {
block-size: 12.5rem;
inline-size: 25rem;
color: #bc71c9;
}
```
---
# components/LineClamp
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
Component cuts overflown text with "..." and shows it by hover
### Example
```html
Lorem ipsum Gaudeamus igitur
Carpe diem
Veni, vidi, vici
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [content] | `PolymorpheusContent` | full content inside |
| [lineHeight] | `number` | height of single line, used to limit component's height. |
| [linesLimit] | `number` | number of visible lines |
| [showHint] | `boolean` | show hint on hover when content is overflown |
| [style.max-width.px] | `number` | value of max-width |
### API - Outputs
| Event | Type | Description |
|-------|------|-------------|
| (overflownChange) | `boolean` | when all content is visible. |
### Usage Examples
#### Styles change
**Template:**
```html
Use white-space: nowrap to expand to the right
Daenerys of the House Targaryen, the First of Her Name, The Unburnt, Queen of the Andals, the Rhoynar and the First Men, Queen of Meereen, Khaleesi of the Great Grass Sea, Protector of the Realm, Lady Regent of the Seven Kingdoms, Breaker of Chains and Mother of Dragons
Jorah Mormont of House Mormont, Lord of Bear Island
DRAFT Davis, Julia
@if (!value()) { Load dynamic value }
@if (value$ | async; as value) { } @else { Loading... }
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component, inject, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {tuiWatch} from '@taiga-ui/cdk';
import {TuiButton, TuiNotification} from '@taiga-ui/core';
import {TuiChip, TuiLineClamp} from '@taiga-ui/kit';
import {map, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton, TuiChip, TuiLineClamp, TuiNotification],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly isE2E = inject(WA_IS_E2E);
protected readonly value = signal('');
protected value$ = timer(this.isE2E ? 0 : 4000).pipe(
map(() => `${'async fake value, '.repeat(10)}end!`),
tuiWatch(),
);
public update(): void {
this.value.set(
'Daenerys of the House Targaryen, the First of Her Name, The Unburnt, Queen of the Andals, the Rhoynar and the First Men, Queen of Meereen, Khaleesi of the Great Grass Sea, Protector of the Realm, Lady Regent of the Seven Kingdoms, Breaker of Chains and Mother of Dragons',
);
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
.island {
inline-size: 20rem;
margin-block-end: 1rem;
padding: 1rem;
border: 1px solid var(--tui-border-normal);
border-radius: 1rem;
box-sizing: border-box;
}
.hint {
font: var(--tui-typography-body-s);
line-height: inherit;
}
.no-wrap {
white-space: nowrap;
}
.wrapper {
position: relative;
inset-block-start: 0;
inset-inline-start: 0;
inline-size: 100%;
margin: 0 auto;
border: 1px solid var(--tui-border-normal);
background-color: var(--tui-background-base);
}
.result {
display: flex;
inline-size: 100%;
block-size: 3.5rem;
align-items: center;
.content {
padding: 0 1rem;
}
}
```
#### Expanding
**Template:**
```html
Toggle
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiLineClamp} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiLineClamp],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected linesLimit = 2;
protected toggle(): void {
this.linesLimit = this.collapsed ? 12 : 2;
}
private get collapsed(): boolean {
return this.linesLimit === 2;
}
}
```
**LESS:**
```less
.island {
max-inline-size: 20rem;
padding: 1rem;
border: 1px solid var(--tui-border-normal);
border-radius: 1.75rem;
}
.clamp {
pointer-events: none;
}
```
#### Resize parent container
**Template:**
```html
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.
```
**TypeScript:**
```ts
import {ChangeDetectorRef, Component, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_WINDOW} from '@ng-web-apis/common';
import {TuiLineClamp} from '@taiga-ui/kit';
@Component({
imports: [TuiLineClamp],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly win = inject(WA_WINDOW);
private readonly cdr = inject(ChangeDetectorRef);
protected lineHeight = Number.NaN;
protected lineLimit = Number.NaN;
protected getDynamicLineHeight(element: HTMLDivElement): number {
return Number.parseInt(this.win.getComputedStyle(element).lineHeight, 10);
}
protected getDynamicLineLimit(element: HTMLDivElement): number {
return Math.floor(element.offsetHeight / 24);
}
protected onResize(element: HTMLDivElement): void {
this.lineHeight = this.getDynamicLineHeight(element);
this.lineLimit = this.getDynamicLineLimit(element);
this.cdr.detectChanges();
}
}
```
**LESS:**
```less
.example {
min-block-size: 15rem;
max-block-size: 15rem;
}
.line-clamp-box {
block-size: 5.75rem;
min-block-size: 1.5rem;
resize: both;
overflow: auto;
padding: 0.5rem;
border-radius: var(--tui-radius-l);
border: 1px solid var(--tui-border-normal);
}
```
#### Clamp inside dropdown
**Template:**
```html
@for (text of texts; track text) {
}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiDataList, TuiDropdown, TuiLink} from '@taiga-ui/core';
import {TuiChevron, TuiLineClamp} from '@taiga-ui/kit';
function randomString(len: number): string {
const charSet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let randomString = '';
for (let i = 0; i < len; i++) {
const randomPoz = Math.floor(Math.random() * charSet.length);
// eslint-disable-next-line unicorn/prefer-string-slice
randomString += charSet.substring(randomPoz, randomPoz + 1);
}
return randomString;
}
@Component({
imports: [TuiChevron, TuiDataList, TuiDropdown, TuiLineClamp, TuiLink],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected open = false;
protected texts = [
randomString(100),
"Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum.",
randomString(500),
];
}
```
**LESS:**
```less
.dropdown-button {
inline-size: 16rem;
}
```
#### Custom content workaround
**Template:**
```html
1. Display only the first line, in a popup display remaining lines.
2. Do not use `tui-line-clamp`, use `text-overflow: ellipsis` instead.
{{ user.email }}
User ID: {{ user.id }}
First Name: {{ user.firstName }}
Last Name: {{ user.lastName }}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiHint} from '@taiga-ui/core';
import {TuiLineClamp} from '@taiga-ui/kit';
interface User {
email: string;
firstName: string;
id: string;
lastName: string;
}
@Component({
imports: [TuiHint, TuiLineClamp],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly user: User = {
id: '5a006cb3-2b69-4b23',
email: 'extremely.long.information@example.com',
firstName: 'John',
lastName: 'Doe',
};
}
```
**LESS:**
```less
.island {
inline-size: 20rem;
margin-block-end: 1rem;
box-sizing: border-box;
padding: 1rem;
border: 1px solid var(--tui-border-normal);
border-radius: 1rem;
}
.email {
margin: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
```
#### Virtual content
**Template:**
```html
#{{ user.id }}:
{{ user.email }}
User ID: {{ user.id }}
First Name: {{ user.firstName }}
Last Name: {{ user.lastName }}
```
**TypeScript:**
```ts
import {
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
CdkVirtualScrollViewport,
} from '@angular/cdk/scrolling';
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiScrollControls, TuiScrollRef} from '@taiga-ui/core';
import {TuiLineClamp} from '@taiga-ui/kit';
@Component({
imports: [
CdkFixedSizeVirtualScroll,
CdkVirtualForOf,
CdkVirtualScrollViewport,
TuiLineClamp,
TuiScrollControls,
TuiScrollRef,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly names = [
'Time',
'Past',
'Future',
'Dev',
'Fly',
'Flying',
'Soar',
'Soaring',
'Power',
'Falling',
'Fall',
'Jump',
'Cliff',
'Mountain',
'Rend',
'Red',
'Blue',
'Green',
'Yellow',
'Gold',
'Demon',
'Demonic',
'Panda',
'Cat',
'Kitty',
'Kitten',
'Zero',
'Memory',
'Trooper',
'XX',
'Bandit',
'Fear',
'Light',
'Glow',
'Tread',
'Deep',
'Deeper',
'Deepest',
'Mine',
'Your',
'Worst',
'Enemy',
'Hostile',
'Force',
'Video',
'Game',
'Donkey',
'Mule',
'Colt',
'Cult',
'Cultist',
'Magnum',
'Gun',
'Assault',
'Recon',
'Trap',
'Trapper',
'Redeem',
'Code',
'Script',
'Writer',
'Near',
'Close',
'Open',
'Cube',
'Circle',
'Geo',
'Genome',
'Germ',
'Shot',
'Echo',
'Beta',
'Alpha',
'Gamma',
'Omega',
'Seal',
'Squid',
'Money',
'Cash',
'Lord',
'King',
'Duke',
'Rest',
'Fire',
'Flame',
'Morrow',
'Break',
'Breaker',
'Numb',
'Ice',
'Cold',
'Rotten',
'Sick',
'Sickly',
'Janitor',
'Camel',
'Rooster',
'Sand',
'Desert',
'Dessert',
'Hurdle',
'Racer',
'Eraser',
'Erase',
'Big',
'Small',
'Short',
'Tall',
'Sith',
'Bounty',
'Hunter',
'Cracked',
'Broken',
'Sad',
'Happy',
'Joy',
'Joyful',
'Crimson',
'Destiny',
'Deceit',
'Lies',
'Lie',
'Honest',
'Destined',
'Hawk',
'Eagle',
'Hawker',
'Walker',
'Zombie',
'Sarge',
'Capt',
'Captain',
'Punch',
'One',
'Two',
'Uno',
'Slice',
'Slash',
'Melt',
'Melted',
'Melting',
'Fell',
'Wolf',
'Hound',
'Legacy',
'Sharp',
'Dead',
'Mew',
'Chuckle',
'Bubba',
'Bubble',
'Sandwich',
'Smasher',
'Extreme',
'Multi',
'Universe',
'Ultimate',
'Death',
'Ready',
'Monkey',
'Elevator',
'Wrench',
'Grease',
'Head',
'Theme',
'Grand',
'Cool',
'Kid',
'Boy',
'Girl',
'Vortex',
'Paradox',
];
protected readonly users = Array.from({length: 10_000}, (_, index) => {
const firstName = this.names[Math.floor(Math.random() * this.names.length)] ?? '';
const lastName =
this.names[Math.floor((Math.random() * this.names.length) / 2)] ?? '';
return {
id: index + 1,
firstName,
lastName,
email: `${firstName}_${lastName}@gmail.com`.toLowerCase(),
} as const;
});
}
```
**LESS:**
```less
cdk-virtual-scroll-viewport {
block-size: 12.5rem;
border: 1px solid;
overscroll-behavior: none;
}
div {
block-size: 3.125rem;
padding: 0.625rem;
box-sizing: border-box;
overscroll-behavior: none;
}
```
#### Custom font-size and line-height
**Template:**
```html
Toggle is overflown: {{ hasOverflownContent() }}
Daenerys of the House Targaryen, the First of Her Name, The Unburnt, Queen of the Andals, the Rhoynar and the First Men, Queen of Meereen, Khaleesi
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiLineClamp} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiLineClamp],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly hasOverflownContent = signal(null);
protected linesLimit = 2;
protected toggle(): void {
this.linesLimit = this.collapsed ? 12 : 2;
}
private get collapsed(): boolean {
return this.linesLimit === 2;
}
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiLineClamp} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiLineClamp],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected linesLimit = 1;
protected lineHeight = 24;
protected maxWidth = 100;
protected content = '';
protected showHint = true;
protected readonly examples = [
'Styles change',
'Expanding',
'Resize parent container',
'Clamp inside dropdown',
'Custom content workaround',
'Virtual content',
'Custom font-size and line-height',
];
}
```
### LESS
```less
.text {
max-inline-size: 60%;
}
```
---
# components/LineDaysChart
- **Package**: `ADDON-CHARTS`
- **Type**: components
- **Version**: 5.0.0
Line chart but for days
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [dots] | `boolean` | show dots on chart |
| [hintContent] | `PolymorpheusContent` | |
| [height] | `number` | axis Y range, pixel scale is 1:1 |
| [y] | `number` | start of Y axis |
| [smoothingFactor] | `number` | smoothing factor from 0 to 99 |
| [value] | `[TuiDay, number][]` | array of data |
| [xStringify] | `TuiStringHandler \| null` | function to stringify a value number to a string in axis X hint |
| [yStringify] | `TuiStringHandler \| null` | function to stringify a value number to a string in axis Y hint |
### Usage Examples
#### Basic
**Template:**
```html
Range
```
**TypeScript:**
```ts
import {Component, computed, inject, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiAxes, TuiLineDaysChart} from '@taiga-ui/addon-charts';
import {
TuiDay,
type TuiDayLike,
TuiDayRange,
TuiMonth,
type TuiStringHandler,
} from '@taiga-ui/cdk';
import {TUI_MONTHS, TuiTextfield} from '@taiga-ui/core';
import {TuiInputDateRange} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiAxes, TuiInputDateRange, TuiLineDaysChart, TuiTextfield],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly isE2E = inject(WA_IS_E2E);
private readonly months = inject(TUI_MONTHS);
protected readonly maxLength: TuiDayLike = {month: 12};
protected readonly range = signal(
new TuiDayRange(TuiDay.currentLocal(), TuiDay.currentLocal().append({year: 1})),
);
protected readonly value = computed(({to, from} = this.range()) =>
Array.from({length: TuiDay.lengthBetween(from, to) + 1}).reduce<
ReadonlyArray<[TuiDay, number]>
>(
(array, _, i) => [
...array,
[
from.append({day: i}),
this.isE2E
? 100
: (i ? (array[i - 1]?.[1] ?? 0) : 100) + Math.random() * 10 - 5,
],
],
[],
),
);
protected readonly labels = computed(({to, from} = this.range()) => [
...Array.from(
{length: TuiMonth.lengthBetween(from, to) + 1},
(_, i) => this.months()[from.append({month: i}).month] ?? '',
),
null,
]);
protected readonly xStringify = computed>(
() =>
({month, day}) =>
`${this.months()[month]}, ${day}`,
);
protected readonly yStringify: TuiStringHandler = (y) =>
`${(10 * y).toLocaleString('en-US', {maximumFractionDigits: 0})} $`;
}
```
**LESS:**
```less
:host {
display: block;
inline-size: 50rem;
}
.axes {
block-size: 12.5rem;
color: #bc71c9;
}
```
#### Complex
**Template:**
```html
TuiLineDaysChart is used to show data of several months to simplify working with different number of days in months
Data
Visible range
@for (chart of days(); track chart) {
Chart {{ $index + 1 }}
}
@if (getWidth(show()) > 90) { @for (chart of days(); track chart) { } } @else { @for (chart of days(); track chart) { } }
{{ getDate(data[0][0], range().from) }}
@for (point of data; track point) {
${{ point[1].toFixed(0) }}
}
```
**TypeScript:**
```ts
import {Component, computed, inject, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {
TuiAxes,
TuiLineChart,
TuiLineDaysChart,
TuiLineDaysChartHint,
} from '@taiga-ui/addon-charts';
import {
TuiDay,
type TuiDayLike,
TuiDayRange,
TuiFilterPipe,
type TuiMapper,
TuiMapperPipe,
type TuiMatcher,
TuiMonth,
} from '@taiga-ui/cdk';
import {TUI_MONTHS, TuiNotification, type TuiPoint, TuiTextfield} from '@taiga-ui/core';
import {TuiInputDateRange} from '@taiga-ui/kit';
@Component({
imports: [
FormsModule,
TuiAxes,
TuiFilterPipe,
TuiInputDateRange,
TuiLineChart,
TuiLineDaysChart,
TuiLineDaysChartHint,
TuiMapperPipe,
TuiNotification,
TuiTextfield,
],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Example {
private readonly isE2E = inject(WA_IS_E2E);
private readonly months = inject(TUI_MONTHS);
protected readonly data = signal(
new TuiDayRange(TuiDay.currentLocal(), TuiDay.currentLocal().append({month: 5})),
);
protected readonly show = signal(this.data());
protected readonly days = computed(() => this.random(this.data()));
protected readonly maxLength: TuiDayLike = {month: 6};
protected readonly range = computed(() => {
const range = this.show();
const {from, to} = range;
const length = TuiDay.lengthBetween(from, to);
const dayOfWeekFrom = from.dayOfWeek();
const dayOfWeekTo = to.dayOfWeek();
const mondayFrom = dayOfWeekFrom ? from.append({day: 7 - dayOfWeekFrom}) : from;
const mondayTo = dayOfWeekTo ? to.append({day: 7 - dayOfWeekTo}) : to;
const mondaysLength = TuiDay.lengthBetween(mondayFrom, mondayTo);
if (length > 90) {
return range;
}
if (length > 60) {
return new TuiDayRange(
mondayFrom,
mondayTo.append({day: mondaysLength % 14}),
);
}
return length > 14
? new TuiDayRange(mondayFrom, mondayTo)
: new TuiDayRange(from, to.append({day: length % 2}));
});
protected readonly labels = computed(() => {
const {from, to} = this.show();
const length = TuiDay.lengthBetween(from, to);
const months = this.months();
if (length > 90) {
return [
...Array.from(
{length: TuiMonth.lengthBetween(from, to) + 1},
(_, i) => months[from.append({month: i}).month] ?? '',
),
'',
];
}
const range = Array.from({length}, (_, day) => from.append({day}));
const mondays = onlyMondays(range);
const days = range.map(String);
if (length > 60) {
return [...even(mondays), ''];
}
if (length > 14) {
return [...mondays, ''];
}
return length > 7 ? [...even(days), ''] : [...days, ''];
});
protected getWidth({from, to}: TuiDayRange): number {
return TuiDay.lengthBetween(from, to);
}
protected getDate(day: TuiDay | number, date: TuiDay): TuiDay {
return day instanceof TuiDay ? day : date.append({day});
}
protected readonly filter: TuiMatcher<[readonly [TuiDay, number], TuiDayRange]> = (
[day],
{from, to},
) => day.daySameOrAfter(from) && day.daySameOrBefore(to);
protected readonly toNumbers: TuiMapper<
[ReadonlyArray, TuiDayRange],
readonly TuiPoint[]
> = (days, {from}) =>
days.map(([day, value]) => [TuiDay.lengthBetween(from, day), value]);
private generateRandomData(
{from, to}: TuiDayRange,
initial: number,
): ReadonlyArray<[TuiDay, number]> {
return Array.from({length: TuiDay.lengthBetween(from, to) + 1})
.reduce>(
(array, _, i) => [
...array,
[
from.append({day: i}),
this.isE2E
? initial
: Math.max(
(i ? (array[i - 1]?.[1] ?? 0) : initial) +
Math.random() * 10 -
5,
0,
),
],
],
[],
)
.filter(([day]) => day.dayOfWeek() < 5);
}
private random(data: TuiDayRange): ReadonlyArray> {
return [
this.generateRandomData(data, 100),
this.generateRandomData(data, 75),
this.generateRandomData(data, 50),
];
}
}
function onlyMondays(range: readonly TuiDay[]): readonly string[] {
return range.filter((day) => !day.dayOfWeek()).map(String);
}
function even(array: readonly T[]): readonly T[] {
return array.filter((_, i) => !(i % 2));
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
.color() {
color: var(--tui-chart-categorical-01);
&:first-child {
color: var(--tui-chart-categorical-08);
}
&:last-child {
color: var(--tui-chart-categorical-12);
}
}
.axes {
block-size: 12.5rem;
}
.controls {
display: flex;
tui-textfield {
flex: 1;
}
}
.legend {
display: flex;
justify-content: center;
align-items: center;
}
.item {
.color();
display: flex;
align-items: center;
margin: 0 0.75rem;
&::before {
content: '';
border-block-end: 0.125rem solid;
inline-size: 1rem;
margin-inline-end: 0.5rem;
}
}
.name {
color: var(--tui-text-primary);
}
.value {
color: #fff;
}
.chart {
.color();
.fullsize();
}
```
### TypeScript
```ts
import {Component, computed, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiAxes, TuiLineDaysChart} from '@taiga-ui/addon-charts';
import {type TuiContext, TuiDay, type TuiStringHandler} from '@taiga-ui/cdk';
import {TUI_MONTHS} from '@taiga-ui/core';
import {type PolymorpheusContent} from '@taiga-ui/polymorpheus';
@Component({
imports: [TuiAxes, TuiDemo, TuiLineDaysChart],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected readonly months = inject(TUI_MONTHS);
protected readonly examples = ['Basic', 'Complex'];
protected readonly valueVariants: ReadonlyArray> = [
Array.from({length: 91}).reduce>(
(array, _, i) => [
...array,
[
new TuiDay(2020, 0, 1).append({day: i}),
(i ? (array[i - 1]?.[1] ?? 0) : 100) + Math.random() * 20 - 10,
],
],
[],
),
[
[new TuiDay(2020, 1, 10), 10],
[new TuiDay(2020, 1, 15), 150],
[new TuiDay(2020, 1, 17), 10],
[new TuiDay(2020, 1, 20), 10],
[new TuiDay(2020, 1, 25), 150],
[new TuiDay(2020, 1, 27), 10],
],
];
protected value = this.valueVariants[0]!;
protected readonly labels = computed(() =>
Array.from({length: 4}, (_, i) => this.months()?.[i] ?? ''),
);
protected readonly yStringifyVariants: ReadonlyArray> = [
(y) => `${(10 * y).toLocaleString('en-US', {maximumFractionDigits: 0})} $`,
];
protected readonly xStringifyVariants = computed(() => [
({month, day}: TuiDay) => `${this.months()?.[month]}, ${day}`,
]);
protected readonly hintContentVariants = computed(() => [
'',
({$implicit}: {$implicit: [TuiDay, number]}) =>
`${this.months()?.[$implicit[0].month]}, ${$implicit[0].day}\n${(
10 * $implicit[1]
).toLocaleString('en-US', {maximumFractionDigits: 0})} $`,
]);
protected yStringify: TuiStringHandler | null = null;
protected xStringify: TuiStringHandler | null = null;
protected hintContent: PolymorpheusContent> = '';
protected dots = false;
protected smoothingFactor = 0;
protected y = 0;
protected height = 200;
}
```
### LESS
```less
.axes {
block-size: 12.5rem;
inline-size: 45.5rem;
color: #bc71c9;
}
```
---
# components/Link
- **Package**: `CORE`
- **Type**: components
- **Version**: 5.0.0
Link component. It has focus highlight and can be customized with an icon Use `textContent` binding or wrapping entire content with a `span` to avoid extra whitespace introduced by browser to `a` tag when closing tag wraps to a new line — otherwise distance to the icons will be bigger than it should be
### Usage Examples
#### Basic
Can be used on native `a` tag with `routerLink` or `href`
**Template:**
```html
Link
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {RouterLink} from '@angular/router';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {DemoRoute} from '@demo/routes';
import {TuiLink} from '@taiga-ui/core';
@Component({
imports: [RouterLink, TuiLink],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly routes = DemoRoute;
}
```
#### Icons
Can also be used on buttons as well as with icons on either side.
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLink} from '@taiga-ui/core';
@Component({
imports: [TuiLink],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Appearances
Different color and underline styles.
**Template:**
```html
Links by default have
action
appearance which you can override with action-grayscale or
If you use no appearance, links would
inherit text color
in tandem with an underline decoration to make them visible.
It would also work well together with
dark theme
and other text colors
If you want, you can
enable
dashed underline for any appearance using text-decoration-line: underline or switch inherited link underline to dashed style by text-decoration-style: dashed
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLink} from '@taiga-ui/core';
@Component({
imports: [TuiLink],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
[tuiTheme='dark'] {
background: var(--tui-background-base);
color: var(--tui-text-secondary);
padding: 1rem;
margin: 1rem -1rem;
}
```
#### Long text
Multi line wrapping also works with icons.
**Template:**
```html
Link can be written inline in text.
Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the standard dummy text block and if no extra whitespace is introduced, the icon would wrap together with the last word inside the tag
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLink} from '@taiga-ui/core';
@Component({
imports: [TuiLink],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = ['Basic', 'Icons', 'Appearances', 'Long text'];
}
```
---
# components/List
- **Package**: `LAYOUT`
- **Type**: components
- **Version**: 5.0.0
### Usage Examples
#### Bulleted
**Template:**
```html
Body L
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
Body M
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
UI S
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiHeader, TuiList} from '@taiga-ui/layout';
@Component({
imports: [TuiHeader, TuiList],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Numbered
**Template:**
```html
Body L
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
Body M
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
UI S
List of Continents
Eurasia
Africa
Australia
Antarctica
North America
South America
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiHeader, TuiList} from '@taiga-ui/layout';
@Component({
imports: [TuiHeader, TuiList],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Definition
**Template:**
```html
@for (item of data | keyvalue; track $index) { {{ item.key }}
{{ item.value }} }
@for (item of data | keyvalue; track $index) { {{ item.key }}
{{ item.value }} }
```
**TypeScript:**
```ts
import {KeyValuePipe} from '@angular/common';
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiHeader, TuiList} from '@taiga-ui/layout';
@Component({
imports: [KeyValuePipe, TuiHeader, TuiList],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly data = {
Name: 'Taiga UI',
Version: '5.22.0',
Component: 'List',
};
}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
```
#### Nested
**Template:**
```html
First level
Second level
Second level
Third level
Third level
Second level
Third level
Third level
Fourth level
Fourth level
First level
First level
Second level
Second level
First level
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTitle} from '@taiga-ui/core';
import {TuiHeader, TuiList} from '@taiga-ui/layout';
@Component({
imports: [TuiHeader, TuiList, TuiTitle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Long text
**Template:**
```html
If a list item is long, the bullet stays aligned to the top.
There are no restrictions on the text length at all. You can write as much text as you want.
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiList} from '@taiga-ui/layout';
@Component({
imports: [TuiList],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Custom color
**Template:**
```html
All bullets in this list share the same custom color
Set the--tui-list-markervariable on the list itself
No need to override the bullet background manually
Bullets can also be recolored individually
This one is a warning
This one is positive
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiList} from '@taiga-ui/layout';
@Component({
imports: [TuiList],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = [
'Bulleted',
'Numbered',
'Definition',
'Nested',
'Long text',
'Custom color',
];
}
```
---
# components/Loader
- **Package**: `CORE`
- **Type**: components
- **Version**: 5.0.0
### Example
```html
Colonel Trautman: It's over Johnny. It's over!
Rambo: Nothing is over! Nothing! You just don't turn it off! It wasn't my war! You asked me I didn't ask you! And I did what I had to do to win, for somebody who wouldn't let us win! Then I come back to the world, and I see all those maggots at the airport, protestin' me, spittin', callin' me a baby killer and all kinds of vile crap! Who are they to protest me?! Huh?! Who are they?! Unless they been me and been there and know what the hell they yellin' about!
Loading
You can use a template with
HTML here
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [loading] | `boolean` | show/hide loader |
| [inheritColor] | `boolean` | inherit parent color |
| [overlay] | `boolean` | content overlay when loader is showed |
| [size] | `TuiSizeXS \| TuiSizeXL` | — |
| [textContent] | `PolymorpheusContent` | custom content under loader |
### Usage Examples
#### With inherited background color
**Template:**
```html
I don't know who you are. I don't know what you want. If you are looking for ransom, I can tell you I don't have money. But what I do have are a very particular set of skills; skills I have acquired over a very long career. Skills that make me a nightmare for people like you. If you let my daughter go now, that'll be the end of it. I will not look for you, I will not pursue you. But if you don't, I will look for you, I will find you, and I will kill you.
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader} from '@taiga-ui/core';
@Component({
imports: [TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: block;
background: var(--tui-background-accent-1);
box-shadow: 0 0 0 100rem var(--tui-background-accent-1);
color: var(--tui-text-primary-on-accent-1);
}
```
#### With content overlay
**Template:**
```html
I don't know who you are. I don't know what you want. If you are looking for ransom, I can tell you I don't have money. But what I do have are a very particular set of skills; skills I have acquired over a very long career. Skills that make me a nightmare for people like you. If you let my daughter go now, that'll be the end of it. I will not look for you, I will not pursue you. But if you don't, I will look for you, I will find you, and I will kill you.
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader} from '@taiga-ui/core';
@Component({
imports: [TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: block;
background: #3e4757;
box-shadow: 0 0 0 100rem #3e4757;
color: var(--tui-background-base);
}
```
#### Options
**Template:**
```html
This example demonstrates how to configure loader options using tuiLoaderOptionsProvider. The loader is configured with size 'l', overlay enabled, and inheritColor disabled.
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader, tuiLoaderOptionsProvider} from '@taiga-ui/core';
@Component({
imports: [TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [
tuiLoaderOptionsProvider({
size: 'l',
inheritColor: false,
overlay: true,
}),
],
})
export default class Example {}
```
**LESS:**
```less
.inline-flex {
display: inline-flex;
}
```
#### Custom stroke width
Use css-variable `--tui-thickness` to customize width of the circle stroke. By default, it is 1/12 of diameter.
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader, tuiLoaderOptionsProvider} from '@taiga-ui/core';
@Component({
imports: [TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [tuiLoaderOptionsProvider({size: 'xl'})],
})
export default class Example {}
```
**LESS:**
```less
tui-loader {
min-inline-size: 3.5rem;
&:nth-child(1) {
/**
Don't use `rem` units if you support Safari.
Safari doesn't support `rem` units for `stroke-*` properties of ` `.
Use `em` units => they will be interpreted as `rem` ones
(` ` has `font-size: 1rem`).
Or just use simple `px` units.
*/
--tui-thickness: 0.125em;
}
&:nth-child(2) {
--tui-thickness: 0.25rem;
}
&:nth-child(3) {
--tui-thickness: 0.5rem;
}
}
:host {
display: flex;
gap: 2rem;
}
```
#### Custom size
Use css-variable `--tui-diameter` to customize the loader size instead of one of the predefined `[size]` options.
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader} from '@taiga-ui/core';
@Component({
imports: [TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
align-items: center;
gap: 2rem;
}
```
### TypeScript
```ts
import {Component, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
import {TuiLoader, type TuiSizeXS, type TuiSizeXXL} from '@taiga-ui/core';
import {type PolymorpheusContent} from '@taiga-ui/polymorpheus';
@Component({
imports: [TuiDemo, TuiLoader],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected readonly textTemplate = viewChild('textTemplate');
protected readonly routes = DemoRoute;
protected readonly examples = [
'With inherited background color',
'With content overlay',
'Options',
'Custom stroke width',
'Custom size',
];
protected loading = true;
protected inheritColor = false;
protected overlay = false;
protected readonly sizeVariants: ReadonlyArray = [
'xs',
's',
'm',
'l',
'xl',
'xxl',
];
protected size = this.sizeVariants[2]!;
protected selectedTemplate = '';
protected readonly textVariants = ['', 'template', 'string'];
protected get template(): PolymorpheusContent {
switch (this.selectedTemplate) {
case 'string': {
return 'string';
}
case 'template': {
return this.textTemplate() || '';
}
default: {
return '';
}
}
}
}
```
### LESS
```less
.example {
padding: 1.25rem;
background: var(--tui-background-neutral-1);
}
```
---
# components/Message
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
Message component is used to display message block.
### Example
```html
Message
```
### Usage Examples
#### Basic
**Template:**
```html
Incoming message
Outgoing message
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiMessage],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
gap: 1rem;
}
```
#### Custom color
**Template:**
```html
I'm default 🤘
And I'm colored 🤘🏿
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiMessage],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
[tuiAppearance][data-appearance='custom'] {
background: rgba(175, 242, 24, 1);
}
.container {
display: flex;
inline-size: 100%;
}
.incoming {
justify-content: flex-start;
}
.outgoing {
justify-content: flex-end;
}
```
#### With link
**Template:**
```html
All relevant guides in this project with a lot of examples
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLink} from '@taiga-ui/core';
import {TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiLink, TuiMessage],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Chat messages
**Template:**
```html
Fade
Left Comment with very long content
Right Comment with very long content
Multiline
Very long content that goes on the second line
Very long content that goes on the second line
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiFade, TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiFade, TuiMessage],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 1rem;
}
.fade {
max-inline-size: 11rem;
}
.multiline {
white-space: initial;
}
.container {
display: flex;
inline-size: 100%;
}
.incoming {
justify-content: flex-start;
> [tuiMessage] {
margin-inline-start: 1rem;
margin-inline-end: 2rem;
}
}
.outgoing {
justify-content: flex-end;
> [tuiMessage] {
margin-inline-start: 2rem;
margin-inline-end: 1rem;
}
}
```
#### Inside cells
**Template:**
```html
Alexander I.
USD
Don't deny yourself anything
– $10 000
September 6
For game
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiCell, TuiTitle} from '@taiga-ui/core';
import {TuiAvatar, TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiAvatar, TuiCell, TuiMessage, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
[tuiCell] {
max-inline-size: 25rem;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {TuiDocAppearance} from '@demo/components/appearance';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiMessage} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiDocAppearance, TuiMessage],
templateUrl: './index.html',
changeDetection,
})
export default class Example {
protected readonly examples = [
'Basic',
'Custom color',
'With link',
'Chat messages',
'Inside cells',
];
}
```
---
# components/Meter
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.22.0
Native styled Meter tag, using the same attributes as native version.
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [min] | `number` | minimal value, lower bound |
| [max] | `number` | maximal value, upper bound |
| [low] | `number` | first color point (yellow if optimum is at a lower end, red if it is at the higher end) |
| [high] | `number` | second color point (red if optimum is at a lower end, yellow if it is at the higher end) |
| [optimum] | `number` | third color point (green, representing preferred value) |
| [value] | `number` | current value |
### Usage Examples
#### Increasing
**Template:**
```html
Credit Score
300
666
850
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTitle} from '@taiga-ui/core';
import {TuiBadge, TuiMeter} from '@taiga-ui/kit';
@Component({
imports: [TuiBadge, TuiMeter, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
.ticks-labels {
.tui-slider-ticks-labels();
position: relative;
[tuiBadge] {
position: absolute;
inset-block-end: 0.25rem;
font: var(--tui-typography-body-l);
font-weight: bold;
transform: translateX(calc(-50% * var(--tui-inline)));
}
}
```
#### Decreasing
**Template:**
```html
Resting Heart Rate
The number of times your heart beats per minute when you are completely relaxed, still, and awake
40
60
100
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTitle} from '@taiga-ui/core';
import {TuiBadge, TuiMeter} from '@taiga-ui/kit';
import {TuiHeader} from '@taiga-ui/layout';
@Component({
imports: [TuiBadge, TuiHeader, TuiMeter, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
[tuiMeter] {
margin-block-start: 1rem;
}
.ticks-labels {
.tui-slider-ticks-labels();
position: relative;
[tuiBadge] {
position: absolute;
font: var(--tui-typography-body-l);
font-weight: bold;
transform: translateX(calc(-50% * var(--tui-inline)));
}
}
```
### TypeScript
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiMeter} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiMeter],
templateUrl: './index.html',
changeDetection,
})
export default class Example {
protected readonly min = signal('0');
protected readonly max = signal('1');
protected readonly low = signal('0');
protected readonly high = signal('1');
protected readonly optimum = signal('0.5');
protected readonly value = signal('0');
}
```
---
# components/MobileCalendar
- **Package**: `ADDON-MOBILE`
- **Type**: components
- **Version**: 5.0.0
A calendar for mobile devices. It is used in date picker controls on mobile devices if `tuiMobileCalendar` directive is applied. You can use `TUI_CALENDAR_DATE_STREAM` token to set value from outside (see samples)
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [(value)] | `TuiDay \| TuiDayRange \| readonly TuiDay[] \| null` | — |
| [disabledItemHandler] | `TuiBooleanHandler` | |
| [markerHandler] | `TuiMarkerHandler \| null` | a handler that returns marker colors for a date |
| [max] | `TuiDay` | max date |
| [min] | `TuiDay` | min date |
| [multi] | `boolean` | array of single dates |
| [single] | `boolean` | |
### API - Outputs
| Event | Type | Description |
|-------|------|-------------|
| (cancel) | `void` | output when user clicks Cancel |
| (confirm) | `TuiDayRange \| TuiDay` | output when user clicks Confirm (range or single day) |
### Usage Examples
#### Custom dropdown
**Template:**
```html
Choose a date
{{ date$ | async }}
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component, inject, INJECTOR, Injector} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';
import {FormControl} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {
TUI_CALENDAR_DATE_STREAM,
TuiMobileCalendarDropdownComponent,
} from '@taiga-ui/addon-mobile';
import {tuiControlValue, TuiDay} from '@taiga-ui/cdk';
import {TUI_MONTHS, TuiButton, TuiDialogService} from '@taiga-ui/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
import {combineLatest, map, type Observable} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly dialogs = inject(TuiDialogService);
private readonly injector = inject(INJECTOR);
private readonly months$ = toObservable(inject(TUI_MONTHS));
private readonly control = new FormControl(null);
private readonly dialog$: Observable = this.dialogs.open(
new PolymorpheusComponent(
TuiMobileCalendarDropdownComponent,
Injector.create({
providers: [
{
provide: TUI_CALENDAR_DATE_STREAM,
useValue: tuiControlValue(this.control),
},
],
parent: this.injector,
}),
),
{
appearance: 'fullscreen',
closable: false,
data: {
single: true,
min: TuiDay.currentLocal(),
},
},
);
protected readonly date$ = combineLatest([
tuiControlValue(this.control),
this.months$,
]).pipe(
map(([value, months]) =>
value
? `${months[value.month]} ${value.day}, ${value.year}`
: 'Choose a date',
),
);
protected get empty(): boolean {
return !this.control.value;
}
protected onClick(): void {
this.dialog$.subscribe((value) => this.control.setValue(value));
}
}
```
**LESS:**
```less
.wrapper {
display: flex;
align-items: center;
}
.date {
margin-inline-start: 1rem;
&_empty {
color: var(--tui-text-tertiary);
}
}
```
#### Range
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiMobileCalendar} from '@taiga-ui/addon-mobile';
import {TuiDay} from '@taiga-ui/cdk';
import {tuiCalendarSheetOptionsProvider} from '@taiga-ui/core';
@Component({
imports: [TuiMobileCalendar],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [tuiCalendarSheetOptionsProvider({rangeMode: true})],
})
export default class Example {
protected min = new TuiDay(new Date().getFullYear(), new Date().getMonth(), 1);
protected max = new TuiDay(new Date().getFullYear(), new Date().getMonth(), 10);
}
```
**LESS:**
```less
.example {
block-size: 35rem;
}
```
#### Localization
Use `tuiCalendarOptionsProvider` to change start day of the week (Monday by default)
**Template:**
```html
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiMobileCalendar} from '@taiga-ui/addon-mobile';
import {TuiDay, TuiDayOfWeek} from '@taiga-ui/cdk';
import {tuiCalendarOptionsProvider} from '@taiga-ui/core';
@Component({
imports: [TuiMobileCalendar],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [tuiCalendarOptionsProvider({weekStart: signal(TuiDayOfWeek.Sunday)})],
})
export default class Example {
protected min = TuiDay.currentLocal();
}
```
**LESS:**
```less
tui-mobile-calendar {
max-inline-size: 20rem;
block-size: 30rem;
}
```
#### Custom dropdown (range)
**Template:**
```html
Calendar
{{ date$ | async }}
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component, inject, INJECTOR, Injector} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';
import {FormControl} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {
TUI_CALENDAR_DATE_STREAM,
TuiMobileCalendarDropdownComponent,
} from '@taiga-ui/addon-mobile';
import {tuiControlValue, TuiDay, type TuiDayRange} from '@taiga-ui/cdk';
import {TUI_MONTHS, TuiButton, TuiDialogService} from '@taiga-ui/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
import {combineLatest, map, type Observable} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly dialogs = inject(TuiDialogService);
private readonly injector = inject(INJECTOR);
private readonly months$ = toObservable(inject(TUI_MONTHS));
private readonly control = new FormControl(null);
private readonly dialog$: Observable = this.dialogs.open(
new PolymorpheusComponent(
TuiMobileCalendarDropdownComponent,
Injector.create({
providers: [
{
provide: TUI_CALENDAR_DATE_STREAM,
useValue: tuiControlValue(this.control),
},
],
parent: this.injector,
}),
),
{
appearance: 'fullscreen',
closable: false,
data: {min: new TuiDay(2018, 2, 10)},
},
);
protected readonly date$ = combineLatest([
tuiControlValue(this.control),
this.months$,
]).pipe(
map(([value, months]) => {
if (!value) {
return 'Choose a date range';
}
return value.isSingleDay
? `${months[value.from.month]} ${value.from.day}, ${value.from.year}`
: `${months[value.from.month]} ${value.from.day}, ${value.from.year} - ${
months[value.to.month]
} ${value.to.day}, ${value.to.year}`;
}),
);
protected get empty(): boolean {
return !this.control.value;
}
protected onClick(): void {
this.dialog$.subscribe((value) => this.control.setValue(value));
}
}
```
**LESS:**
```less
.wrapper {
display: flex;
align-items: center;
}
.date {
margin-inline-start: 1rem;
&_empty {
color: var(--tui-text-tertiary);
}
}
```
#### Custom dropdown (multi)
**Template:**
```html
Calendar
{{ date$ | async }}
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component, inject, INJECTOR, Injector} from '@angular/core';
import {toObservable} from '@angular/core/rxjs-interop';
import {FormControl} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {
TUI_CALENDAR_DATE_STREAM,
TuiMobileCalendarDropdownComponent,
} from '@taiga-ui/addon-mobile';
import {tuiControlValue, TuiDay} from '@taiga-ui/cdk';
import {TUI_MONTHS, TuiButton, TuiDialogService} from '@taiga-ui/core';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
import {combineLatest, map, type Observable} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly dialogs = inject(TuiDialogService);
private readonly injector = inject(INJECTOR);
private readonly months$ = toObservable(inject(TUI_MONTHS));
private readonly control = new FormControl(null);
private readonly dialog$: Observable = this.dialogs.open(
new PolymorpheusComponent(
TuiMobileCalendarDropdownComponent,
Injector.create({
providers: [
{
provide: TUI_CALENDAR_DATE_STREAM,
useValue: tuiControlValue(this.control),
},
],
parent: this.injector,
}),
),
{
appearance: 'fullscreen',
closable: false,
data: {
multi: true,
min: new TuiDay(2018, 2, 10),
},
},
);
protected readonly date$ = combineLatest([
tuiControlValue(this.control),
this.months$,
]).pipe(
map(([value, months]) =>
value?.length
? value
.map((day) => `${months[day.month]} ${day.day}, ${day.year}`)
.join('; ')
: 'Choose dates',
),
);
protected get empty(): boolean {
return !this.control.value?.length;
}
protected onClick(): void {
this.dialog$.subscribe((value) => this.control.setValue(value));
}
}
```
**LESS:**
```less
.wrapper {
display: flex;
align-items: center;
}
.date {
margin-inline-start: 1rem;
&_empty {
color: var(--tui-text-tertiary);
}
}
```
#### Without header
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiMobileCalendar} from '@taiga-ui/addon-mobile';
import {TuiDay} from '@taiga-ui/cdk';
import {TUI_CHOOSE_DAY_OR_RANGE_TEXTS} from '@taiga-ui/kit';
@Component({
imports: [TuiMobileCalendar],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [
{
provide: TUI_CHOOSE_DAY_OR_RANGE_TEXTS,
useValue: null,
},
],
})
export default class Example {
protected min = new TuiDay(new Date().getFullYear(), new Date().getMonth(), 1);
protected max = new TuiDay(new Date().getFullYear(), new Date().getMonth(), 10);
}
```
**LESS:**
```less
:host {
display: block;
block-size: 35rem;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {FormControl} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
import {TUI_CALENDAR_DATE_STREAM, TuiMobileCalendar} from '@taiga-ui/addon-mobile';
import {
TUI_FALSE_HANDLER,
TUI_FIRST_DAY,
TUI_LAST_DAY,
type TuiBooleanHandler,
tuiControlValue,
TuiDay,
type TuiDayRange,
} from '@taiga-ui/cdk';
import {type TuiMarkerHandler} from '@taiga-ui/core';
import {type Observable} from 'rxjs';
@Component({
imports: [TuiDemo, TuiMobileCalendar],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
providers: [
{
deps: [Page],
provide: TUI_CALENDAR_DATE_STREAM,
useFactory: (component: Page): Observable => component.stream,
},
],
})
export default class Page {
protected minVariants = [
TUI_FIRST_DAY,
new TuiDay(2017, 2, 5),
new TuiDay(1900, 0, 1),
];
protected min = this.minVariants[0]!;
protected maxVariants = [
TUI_LAST_DAY,
new TuiDay(2020, 2, 5),
new TuiDay(2300, 0, 1),
];
protected max = this.maxVariants[0]!;
protected single = true;
protected multi = false;
protected readonly disabledItemHandlerVariants: ReadonlyArray<
TuiBooleanHandler
> = [TUI_FALSE_HANDLER, ({day}) => day % 3 === 0];
protected disabledItemHandler = this.disabledItemHandlerVariants[0]!;
protected readonly markerHandlerVariants: readonly TuiMarkerHandler[] = [
({day}) => (day % 2 === 0 ? ['var(--tui-status-positive)'] : []),
];
protected markerHandler: TuiMarkerHandler | null = null;
protected control = new FormControl(null);
protected stream = tuiControlValue(this.control);
protected readonly routes = DemoRoute;
protected readonly examples = [
'Custom dropdown',
'Range',
'Localization',
'Custom dropdown (range)',
'Custom dropdown (multi)',
'Without header',
];
}
```
### LESS
```less
.calendar {
block-size: min(28.75rem, 60vh);
}
```
---
# components/Navigation
- **Package**: `LAYOUT`
- **Type**: components
- **Version**: 5.0.0
Taiga UI is a low level component library with many atomic components that provide great flexibility when combined together. An exception to the general rule is the Navigation component – an opinionated global wrapping navigation that exposes less customization options developed specifically to target data-heavy dashboards with minimal space lost and compact controls.
### Usage Examples
#### Full
Full fledged example showcasing every section of the navigation filled with content, links, menus etc.
**Template:**
```html
A very very long product name
Alpha
Search @if (expanded()) { 12
}
Groups
Settings
Account
Notifications
Privacy
By default ellipsis is used but you can use fade too
More
Write
Categories
Fiction (will close menu)
Non-Fiction
Children
Add
Favorites
{{ expanded() ? 'Collapse' : 'Expand' }}
@for (item of breadcrumbs; track item) { @if ($last) { {{ item }} } @if (!$last) { {{ item }} } }
Default view
Details
Followers
Secondary
Primary
@for (_ of '-'.repeat(10); track $index) {
Sidebar content Use CSS grid to position
}
```
**TypeScript:**
```ts
import {KeyValuePipe, NgTemplateOutlet} from '@angular/common';
import {Component, Directive, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {RouterLink} from '@angular/router';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {DemoRoute} from '@demo/routes';
import {TuiPortals, TuiPortalService, tuiProvide, TuiVCR} from '@taiga-ui/cdk';
import {
TuiButton,
TuiDataList,
TuiDropdown,
TuiIcon,
TuiInput,
TuiLink,
TuiPopupService,
TuiTitle,
} from '@taiga-ui/core';
import {
TuiAvatar,
TuiBadge,
TuiBadgeNotification,
TuiBreadcrumbs,
TuiChevron,
TuiDataListDropdownManager,
TuiFade,
TuiSwitch,
TuiTabs,
} from '@taiga-ui/kit';
import {TuiCardLarge, TuiForm, TuiHeader, TuiNavigation} from '@taiga-ui/layout';
const ICON =
"data:image/svg+xml,%0A%3Csvg width='32' height='32' viewBox='0 0 32 32' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Crect width='32' height='32' rx='8' fill='url(%23paint0_linear_2036_35276)'/%3E%3Cmask id='mask0_2036_35276' style='mask-type:alpha' maskUnits='userSpaceOnUse' x='6' y='5' width='20' height='21'%3E%3Cpath d='M18.2399 9.36607C21.1347 10.1198 24.1992 9.8808 26 7.4922C26 7.4922 21.5645 5 16.4267 5C11.2888 5 5.36726 8.69838 6.05472 16.6053C6.38707 20.4279 6.65839 23.7948 6.65839 23.7948C8.53323 22.1406 9.03427 19.4433 8.97983 16.9435C8.93228 14.7598 9.55448 12.1668 12.1847 10.4112C14.376 8.94865 16.4651 8.90397 18.2399 9.36607Z' fill='url(%23paint1_linear_2036_35276)'/%3E%3Cpath d='M11.3171 20.2647C9.8683 17.1579 10.7756 11.0789 16.4267 11.0789C20.4829 11.0789 23.1891 12.8651 22.9447 18.9072C22.9177 19.575 22.9904 20.2455 23.2203 20.873C23.7584 22.3414 24.7159 24.8946 24.7159 24.8946C23.6673 24.5452 22.8325 23.7408 22.4445 22.7058L21.4002 19.921L21.2662 19.3848C21.0202 18.4008 20.136 17.7104 19.1217 17.7104H17.5319L17.6659 18.2466C17.9119 19.2306 18.7961 19.921 19.8104 19.921L22.0258 26H10.4754C10.7774 24.7006 12.0788 23.2368 11.3171 20.2647Z' fill='url(%23paint2_linear_2036_35276)'/%3E%3C/mask%3E%3Cg mask='url(%23mask0_2036_35276)'%3E%3Crect x='4' y='4' width='24' height='24' fill='white'/%3E%3C/g%3E%3Cdefs%3E%3ClinearGradient id='paint0_linear_2036_35276' x1='0' y1='0' x2='32' y2='32' gradientUnits='userSpaceOnUse'%3E%3Cstop stop-color='%23A681D4'/%3E%3Cstop offset='1' stop-color='%237D31D4'/%3E%3C/linearGradient%3E%3ClinearGradient id='paint1_linear_2036_35276' x1='6.0545' y1='24.3421' x2='28.8119' y2='3.82775' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0.0001' stop-opacity='0.996458'/%3E%3Cstop offset='0.317708'/%3E%3Cstop offset='1' stop-opacity='0.32'/%3E%3C/linearGradient%3E%3ClinearGradient id='paint2_linear_2036_35276' x1='6.0545' y1='24.3421' x2='28.8119' y2='3.82775' gradientUnits='userSpaceOnUse'%3E%3Cstop offset='0.0001' stop-opacity='0.996458'/%3E%3Cstop offset='0.317708'/%3E%3Cstop offset='1' stop-opacity='0.32'/%3E%3C/linearGradient%3E%3C/defs%3E%3C/svg%3E%0A";
// Ignore portal related code, it is only here to position drawer inside the example block
@Directive({
selector: '[portal]',
providers: [tuiProvide(TuiPopupService, TuiPortalService)],
})
class Portal {}
@Component({
imports: [
FormsModule,
KeyValuePipe,
NgTemplateOutlet,
Portal,
RouterLink,
TuiAvatar,
TuiBadge,
TuiBadgeNotification,
TuiBreadcrumbs,
TuiButton,
TuiCardLarge,
TuiChevron,
TuiDataList,
TuiDataListDropdownManager,
TuiDropdown,
TuiFade,
TuiForm,
TuiHeader,
TuiIcon,
TuiInput,
TuiLink,
TuiNavigation,
TuiSwitch,
TuiTabs,
TuiTitle,
TuiVCR,
],
templateUrl: './index.html',
encapsulation,
changeDetection,
providers: [{provide: TuiPortalService, useClass: TuiPopupService}],
})
export default class Example extends TuiPortals {
protected readonly expanded = signal(false);
protected open = false;
protected switch = false;
protected readonly routes = DemoRoute;
protected readonly breadcrumbs = ['Home', 'Angular', 'Repositories', 'Taiga UI'];
protected readonly drawer = {
Components: [
{name: 'Button', icon: ICON},
{name: 'Input', icon: ICON},
{name: 'Tooltip', icon: ICON},
],
Essentials: [
{name: 'Getting started', icon: ICON},
{name: 'Showcase', icon: ICON},
{name: 'Typography', icon: ICON},
],
};
protected handleToggle(): void {
this.expanded.update((e) => !e);
}
}
```
#### Subheader compact
Navigation with page header in a compact mode.
**Template:**
```html
/ Very long repository name
Default view
Contributors
Code
Button
Button
More
Button
Button
Button
@for (_ of '-'.repeat(10); track $index) {
Some random content A subtitle
}
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiThemeColorService} from '@taiga-ui/addon-mobile';
import {
TUI_DARK_MODE,
TuiButton,
TuiDataList,
TuiDropdown,
TuiLink,
TuiTitle,
} from '@taiga-ui/core';
import {TuiAvatar, TuiChevron, TuiFade, TuiSwitch, TuiTabs} from '@taiga-ui/kit';
import {TuiCardLarge, TuiHeader, TuiList, TuiNavigation} from '@taiga-ui/layout';
@Component({
imports: [
FormsModule,
TuiAvatar,
TuiButton,
TuiCardLarge,
TuiChevron,
TuiDataList,
TuiDropdown,
TuiFade,
TuiHeader,
TuiLink,
TuiList,
TuiNavigation,
TuiSwitch,
TuiTabs,
TuiTitle,
],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
private readonly theme = inject(TuiThemeColorService);
protected readonly dark = inject(TUI_DARK_MODE);
protected color = false;
protected onColor(color: boolean): void {
this.theme.color = color ? 'purple' : 'black';
}
}
```
#### Subheader object
Navigation with expanded page header including description and other page details.
**Template:**
```html
@for (_ of '-'.repeat(10); track $index) {
Some random content A subtitle
}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, TuiGroup, TuiIcon, TuiInput, TuiLink, TuiTitle} from '@taiga-ui/core';
import {TuiBadge, TuiBlock, TuiBreadcrumbs, TuiFade, TuiTabs} from '@taiga-ui/kit';
import {TuiCardLarge, TuiHeader, TuiNavigation} from '@taiga-ui/layout';
@Component({
imports: [
FormsModule,
TuiBadge,
TuiBlock,
TuiBreadcrumbs,
TuiButton,
TuiCardLarge,
TuiFade,
TuiGroup,
TuiHeader,
TuiIcon,
TuiInput,
TuiLink,
TuiNavigation,
TuiTabs,
TuiTitle,
],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected current = 'basic';
}
```
#### Customization
Changing colors and mode.
**Template:**
```html
A very very long product name
Search
Groups
Settings
Account
Notifications
Privacy
By default ellipsis is used but you can use fade too
Add
@for (_ of '-'.repeat(10); track $index) {
Some random content A subtitle
}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPortals, TuiPortalService, tuiProvide, TuiVCR} from '@taiga-ui/cdk';
import {TuiButton, TuiPopupService, TuiTitle} from '@taiga-ui/core';
import {TuiChevron, TuiFade} from '@taiga-ui/kit';
import {
TuiCardLarge,
TuiHeader,
tuiLayoutIconsProvider,
TuiNavigation,
} from '@taiga-ui/layout';
@Component({
imports: [
TuiButton,
TuiCardLarge,
TuiChevron,
TuiFade,
TuiHeader,
TuiNavigation,
TuiTitle,
TuiVCR,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
providers: [
tuiLayoutIconsProvider({grid: '@tui.align-justify'}),
// Ignore portal related code, it is only here to position drawer inside the example block
TuiPopupService,
tuiProvide(TuiPortalService, TuiPopupService),
],
})
export default class Example extends TuiPortals {
protected open = true;
}
```
**LESS:**
```less
:host {
// Use TuiThemeColorService instead, this is just for demo purposes
--tui-theme-color: #87ceeb;
}
.drawer {
// Unnecessary when TuiThemeColorService is instead
background: rgb(135, 206, 235);
color: var(--tui-text-primary);
}
aside::before {
display: none;
}
main::before {
content: '';
}
header::after {
display: none;
}
```
#### InputSearch
Using dedicated search component in a header for a spotlight global portal search pattern.
**Template:**
```html
Toggle open: {{ open }}
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TUI_DEFAULT_MATCHER} from '@taiga-ui/cdk';
import {TuiButton, TuiCell, TuiTextfield, TuiTitle} from '@taiga-ui/core';
import {TuiSearchResults} from '@taiga-ui/experimental';
import {TuiAvatar} from '@taiga-ui/kit';
import {TuiInputSearch, TuiNavigation} from '@taiga-ui/layout';
import {filter, map, startWith, switchMap, timer} from 'rxjs';
interface Result {
href: string;
title: string;
subtitle?: string;
icon?: string;
}
const DATA: Record = {
Documents: [
{
title: 'Monty Python',
href: 'https://en.wikipedia.org/wiki/Monty_Python',
},
],
Code: [
{
title: 'Taiga UI',
href: 'https://github.com/taiga-family/taiga-ui',
icon: '@tui.github',
},
{
title: 'Maskito',
href: 'https://github.com/taiga-family/maskito',
icon: '@tui.github',
},
{
title: 'Web APIs with Angular',
href: 'https://github.com/taiga-family/ng-web-apis',
icon: '@tui.github',
},
],
Links: [
{
title: 'Taiga UI',
subtitle: 'Super awesome library',
href: 'https://taiga-ui.dev',
icon: '/assets/images/taiga.svg',
},
{
title: 'Maskito',
href: 'https://maskito.dev',
icon: '@tui.external-link',
},
],
};
@Component({
imports: [
AsyncPipe,
ReactiveFormsModule,
TuiAvatar,
TuiButton,
TuiCell,
TuiInputSearch,
TuiNavigation,
TuiSearchResults,
TuiTextfield,
TuiTitle,
],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class ExampleComponent {
protected readonly popular = ['Taiga UI', 'Maskito', 'Web APIs for Angular'];
protected readonly control = new FormControl('');
protected readonly results$ = this.control.valueChanges.pipe(
filter(Boolean),
switchMap((value: string) =>
timer(2000).pipe(
map(() => this.filter(value)),
startWith(null),
),
),
);
protected open = false;
private filter(query: string): Record {
return Object.entries(DATA).reduce(
(result, [key, value]) => ({
...result,
[key]: value.filter(({title, href, subtitle = ''}) =>
TUI_DEFAULT_MATCHER(`${title}${href}${subtitle}`, query),
),
}),
{},
);
}
}
```
### TypeScript
```ts
import {Component, inject, type OnDestroy} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiDemo} from '@demo/utils';
import {TuiThemeColorService} from '@taiga-ui/addon-mobile';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Page implements OnDestroy {
protected readonly theme = inject(TuiThemeColorService);
protected readonly examples = [
'Full',
'Subheader compact',
'Subheader object',
'Customization',
'InputSearch',
];
constructor() {
this.theme.color = 'black';
}
public ngOnDestroy(): void {
this.theme.color = '#ff7043';
}
}
```
### LESS
```less
:host ::ng-deep header[tuiNavigationHeader][tuiNavigationHeader] {
inline-size: 100%;
}
.sticky-example ::ng-deep .t-demo {
block-size: 30rem;
transform: translate3d(0, 0, 0);
padding: 0 !important;
.custom-portal {
position: sticky;
z-index: 10;
inset-block-start: 0;
block-size: 30rem;
margin-block-end: -30rem;
transform: translate3d(0, 0, 0);
pointer-events: none;
}
}
```
---
# components/Notification
- **Package**: `CORE`
- **Type**: components
- **Version**: 5.0.0
A notification message that can be displayed inline or as an alert
### Example
```html
Title
I'm a subtitle secondary text
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [size] | `TuiSizeS \| TuiSizeL` | — |
| [icon] | `TuiStringHandler \| string` | icon name or a function to match appearance to the icon |
| [label] | `string` | heading |
| [data] | `I` | arbitrary input data for the notification |
| [autoClose] | `TuiNumberHandler \| number` | auto close timeout, 0 for no auto close |
| [closable] | `boolean` | display close button |
| [block] | `'start' \| 'end'` | block position |
| [inline] | `'start' \| 'center' \| 'end'` | inline position |
### Usage Examples
#### Basic
Various sizes, visual options and content examples
**Template:**
```html
Hello world Close
I am title
I am content of the notification and I can even wrap to multiple lines.
So what?
Whatever
Close
Most boring notification
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, TuiLink, TuiNotification, TuiTitle} from '@taiga-ui/core';
@Component({
imports: [TuiButton, TuiLink, TuiNotification, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
```
#### Options
Using DI to set default values
**Template:**
```html
Works with token options
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiNotification, tuiNotificationOptionsProvider} from '@taiga-ui/core';
@Component({
imports: [TuiNotification],
templateUrl: './index.html',
encapsulation,
changeDetection,
providers: [
tuiNotificationOptionsProvider({
icon: '@tui.alarm-clock',
appearance: 'neutral',
size: 's',
}),
],
})
export default class Example {}
```
#### Interactive
Interactive tags and visual customization
**Template:**
```html
Custom icon color Vertically centered
Toasts A notification as a toast
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {RouterLink} from '@angular/router';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {DemoRoute} from '@demo/routes';
import {TuiNotification, TuiTitle} from '@taiga-ui/core';
@Component({
imports: [RouterLink, TuiNotification, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly routes = DemoRoute;
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
button[tuiNotification]::before {
.center-top();
color: var(--tui-text-negative);
}
```
#### Directive
Using directive to show a popover notification
**Template:**
```html
Show
This is a declarative directive alert
Close
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, TuiNotification} from '@taiga-ui/core';
@Component({
imports: [TuiButton, TuiNotification],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly show = signal(false);
}
```
#### Service
Using service to show a popover notification
**Template:**
```html
Show string &ngsp; Show component
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {Router} from '@angular/router';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiAmountPipe} from '@taiga-ui/addon-commerce';
import {type TuiPortalContext} from '@taiga-ui/cdk';
import {
TuiButton,
TuiLink,
type TuiNotificationOptions,
TuiNotificationService,
} from '@taiga-ui/core';
import {injectContext, PolymorpheusComponent} from '@taiga-ui/polymorpheus';
import {switchMap, takeUntil} from 'rxjs';
@Component({
imports: [TuiAmountPipe, TuiButton, TuiLink],
template: `
Your balance:
{{ value | tuiAmount: 'RUB' }}
Submit
Increase
`,
changeDetection,
})
class Alert {
protected readonly context =
injectContext, number>>();
protected value = this.context.data;
protected increaseBalance(): void {
this.value += 10;
}
}
@Component({
selector: 'example-4',
imports: [TuiButton],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly notifications = inject(TuiNotificationService);
protected readonly notification = this.notifications
.open(new PolymorpheusComponent(Alert), {
label: 'Heading is so long that it should be shown in two lines of text',
data: 237,
appearance: 'warning',
autoClose: 0,
})
.pipe(
switchMap((value) =>
this.notifications.open(`Got a value — ${value}`, {label: 'Response'}),
),
takeUntil(inject(Router).events),
);
protected onClick(): void {
this.notification.subscribe();
}
}
```
### TypeScript
```ts
import {Component, inject} from '@angular/core';
import {TuiDocAppearance} from '@demo/components/appearance';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {
TUI_NOTIFICATION_OPTIONS,
TuiButton,
TuiNotificationService,
TuiTitle,
} from '@taiga-ui/core';
@Component({
imports: [TuiButton, TuiDemo, TuiDocAppearance, TuiTitle],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly notifications = inject(TuiNotificationService);
protected readonly options = inject(TUI_NOTIFICATION_OPTIONS);
protected readonly sizes = ['s', 'm', 'l'] as const;
protected readonly icons = [this.options.icon, '@tui.clock', ''];
protected readonly autoCloseVariants = [0, 3000, 5000, 1000, 500];
protected readonly inlineVariants = ['start', 'center', 'end'] as const;
protected readonly blockVariants = ['start', 'end'] as const;
protected readonly examples = [
'Basic',
'Options',
'Interactive',
'Directive',
'Service',
];
public size = this.options.size;
public icon = this.options.icon;
public closable = true;
public label = '';
public autoClose = 0;
public block = this.blockVariants[0];
public inline = this.inlineVariants[2];
}
```
---
# components/NotificationMiddle
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
A modal component to indicate an ongoing blocking action
### Example
```html
Show
Notification example Close
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [closable] | `boolean` | whether the notification can be closed by the user tapping outside or pressing Escape |
### Usage Examples
#### Default
Basic empty loader.
**Template:**
```html
Show
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiNotificationMiddle} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiNotificationMiddle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected open = false;
}
```
#### Content
Changing content and adding text to the card.
**Template:**
```html
Loader with text
Custom icon
Text Icon
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, TuiIconPipe} from '@taiga-ui/core';
import {TuiNotificationMiddle} from '@taiga-ui/kit';
@Component({
imports: [ReactiveFormsModule, TuiButton, TuiIconPipe, TuiNotificationMiddle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected text = false;
protected icon = false;
}
```
#### Transition
Dynamically updating content and closing with timeout. Don't forget Animated directive!
**Template:**
```html
@if (loading()) {
Please wait...
} @else {
Operation successful!
}
Show
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {toObservable, toSignal} from '@angular/core/rxjs-interop';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TUI_FALSE_HANDLER, TuiAnimated} from '@taiga-ui/cdk';
import {TuiButton, TuiLoader} from '@taiga-ui/core';
import {TuiAvatar, TuiNotificationMiddle} from '@taiga-ui/kit';
import {filter, map, startWith, switchMap, take, tap, timer} from 'rxjs';
@Component({
imports: [TuiAnimated, TuiAvatar, TuiButton, TuiLoader, TuiNotificationMiddle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly open = signal(false);
protected readonly loading = toSignal(
toObservable(this.open).pipe(
filter(Boolean),
switchMap(() =>
timer(3000, 2000).pipe(
take(2),
map(TUI_FALSE_HANDLER),
startWith(true),
tap({complete: () => this.open.set(false)}),
),
),
),
);
protected onClick(): void {
this.open.set(true);
}
}
```
#### Service
Design specs require notification to be shown for a minimum of 600ms to prevent visual flickering for short requests.
**Template:**
```html
Show
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiNotificationMiddleService} from '@taiga-ui/kit';
import {bufferTime, first, startWith, switchMap, timer} from 'rxjs';
@Component({
imports: [TuiButton],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
private readonly notification = inject(TuiNotificationMiddleService);
protected onClick(): void {
this.notification
.open('Loading...')
.pipe(
startWith(null),
// Imitating a quick request
switchMap(() => timer(100)),
// Using minimal time to show a notification
bufferTime(600),
first(),
)
.subscribe();
}
}
```
### TypeScript
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
import {TuiButton} from '@taiga-ui/core';
import {TuiNotificationMiddle} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiDemo, TuiNotificationMiddle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Page {
protected routes = DemoRoute;
protected readonly examples = ['Default', 'Content', 'Transition', 'Service'];
protected readonly open = signal(false);
protected closable = false;
}
```
---
# components/Pager
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [size] | `TuiSizeS` | — |
| [index] | `number` | current active dot |
| [max] | `number` | max visible dots |
| [count] | `number` | count of dots |
| [valueContent] | `TemplateRef` | a template for custom view |
### Usage Examples
#### Basic
**Template:**
```html
prev
next
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiPager} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiPager],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected count = 10;
protected index = 0;
protected prev(): void {
this.index = Math.max(this.index - 1, 0);
}
protected next(): void {
this.index = Math.min(this.index + 1, this.count - 1);
}
}
```
#### Icons
**Template:**
```html
@if (index === count - 2) { } @if (index === count - 1) { }
prev
next
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, TuiIcon} from '@taiga-ui/core';
import {TuiPager} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiIcon, TuiPager],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected count = 8;
protected activeIndex = 0;
protected prev(): void {
this.activeIndex = Math.max(this.activeIndex - 1, 0);
}
protected next(): void {
this.activeIndex = Math.min(this.activeIndex + 1, this.count - 1);
}
}
```
#### Dynamic width
**Template:**
```html
prev
next
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiMapperPipe} from '@taiga-ui/cdk';
import {TuiButton} from '@taiga-ui/core';
import {TuiPager, TuiProgress} from '@taiga-ui/kit';
import {map, type Observable, of, takeWhile, tap, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton, TuiMapperPipe, TuiPager, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected static = inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID));
protected count = 10;
protected readonly activeIndex = signal(0);
protected readonly toProgress = (active: boolean): Observable =>
active && !this.static
? timer(0, 100).pipe(
map((i) => i * 5 + 20),
takeWhile((value) => value <= 100),
tap({complete: () => this.next()}),
)
: of(100);
protected prev(): void {
this.activeIndex.update((index) => Math.max(index - 1, 0));
}
protected next(): void {
this.activeIndex.update((index) => Math.min(index + 1, this.count - 1));
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
.progress {
.transition(~'color, inline-size');
inline-size: 1rem;
&:not(&_active) {
inline-size: 0.5rem;
color: transparent;
}
}
```
### TypeScript
```ts
import {Component, type TemplateRef, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiIcon, type TuiSizeS} from '@taiga-ui/core';
import {TuiPager} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiIcon, TuiPager],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly templateRef = viewChild>('templateRef');
protected readonly examples = ['Basic', 'Icons', 'Dynamic width'];
protected index = 0;
protected max = 6;
protected sizes: TuiSizeS[] = ['m', 's'];
protected size = this.sizes[0]!;
protected countVariants = [10, 15, 1, 2, 3, 4, 5, 6, 8, 100];
protected count = this.countVariants[0]!;
protected templateVariants = ['', 'Template'];
protected selectedTemplate = this.templateVariants[0]!;
protected get valueContent(): TemplateRef | undefined {
const template = this.templateRef();
return template && this.selectedTemplate ? template : undefined;
}
}
```
---
# components/Pagination
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
Pagination component enables the user to select a specific page from a range of pages
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [focusable] | `boolean` | accepts focus with keyboard |
| [(index)] | `number` | active page index |
| [length] | `number` | total pages count |
| [size] | `TuiSizeL` | — |
| [activePadding] | `number` | amount of visible pages around active page |
| [sidePadding] | `number` | amount of visible pages at the edges |
### Usage Examples
#### Basic
**Template:**
```html
index
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTextfield} from '@taiga-ui/core';
import {TuiInputSlider, TuiPagination} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiInputSlider, TuiPagination, TuiTextfield],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected length = 64;
protected index = 10;
protected goToPage(index: number): void {
this.index = index;
console.info('New page:', index);
}
}
```
**LESS:**
```less
.slider {
inline-size: 12.5rem;
margin-block-end: 1em;
}
```
#### Visible pages around active
**Template:**
```html
activePadding
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTextfield} from '@taiga-ui/core';
import {TuiInputSlider, TuiPagination} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiInputSlider, TuiPagination, TuiTextfield],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected activePadding = 2;
}
```
**LESS:**
```less
.slider {
inline-size: 12.5rem;
margin-block-end: 1em;
}
```
#### Visible edge pages
**Template:**
```html
sidePadding
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiTextfield} from '@taiga-ui/core';
import {TuiInputSlider, TuiPagination} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiInputSlider, TuiPagination, TuiTextfield],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected sidePadding = 3;
}
```
**LESS:**
```less
.slider {
inline-size: 12.5rem;
margin-block-end: 1em;
}
```
#### Custom
**Template:**
```html
{{ days[index] }}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPagination} from '@taiga-ui/kit';
@Component({
imports: [TuiPagination],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiPagination} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiPagination],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected focusable = true;
protected index = 0;
protected length = 8;
protected readonly sizeVariants = ['m', 'l'] as const;
protected size = this.sizeVariants[1];
protected activePadding = 1;
protected sidePadding = 1;
protected readonly examples = [
'Basic',
'Visible pages around active',
'Visible edge pages',
'Custom',
];
}
```
---
# components/PdfViewer
- **Package**: `LAYOUT`
- **Type**: components
- **Version**: 5.0.0
Wrapper component for viewing PDF files in an iframe PDF display in browsers is handled by each browser independently, using their own homegrown or 3rd-party code, as this is not part of the HTML spec. Keep in mind most mobile devices do not support displaying PDFs in iframe. Check it here . The only way to enforce rendering consistency in all browsers is to do the rendering server-side, bundle your own JS PDF renderer, or use a 3rd-party rendering service. If you want to display it yourself, so you need to rely on `WA_IS_MOBILE` token to provide suitable alternative behavior. For example, you can use third-party service `https://drive.google.com/viewerng/viewer?embedded=true&url=$YOUR_PUBLIC_PATH_TO_PDF` or your own service to render PDF by pdf.js.
### Usage Examples
#### Basic
Note that you need to bypass sanitizer in order to use the URL so make sure you trust it
**Template:**
```html
Open
file.pdf
{{ isMobile ? '' : 'Download' }}
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_MOBILE} from '@ng-web-apis/platform';
import {TuiButton, TuiDialog, TuiTitle} from '@taiga-ui/core';
import {TuiPdfViewer} from '@taiga-ui/layout';
@Component({
imports: [TuiButton, TuiDialog, TuiPdfViewer, TuiTitle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
private readonly sanitizer = inject(DomSanitizer);
protected readonly isMobile = inject(WA_IS_MOBILE);
protected readonly pdf = '/assets/media/taiga.pdf';
protected open = false;
protected readonly url = this.sanitizer.bypassSecurityTrustResourceUrl(
this.isMobile
? `https://drive.google.com/viewerng/viewer?embedded=true&url=https://taiga-ui.dev/${this.pdf}`
: this.pdf,
);
}
```
#### With responsive dialog
Use ResponsiveDialog to display iframe in a SheetDialog on mobile devices
**Template:**
```html
Open
file.pdf
{{ isMobile ? '' : 'Sign' }}
{{ isMobile ? '' : 'Download' }}
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_MOBILE} from '@ng-web-apis/platform';
import {TuiResponsiveDialog} from '@taiga-ui/addon-mobile';
import {TuiButton, TuiNotificationService, TuiTitle} from '@taiga-ui/core';
import {TuiPdfViewer} from '@taiga-ui/layout';
@Component({
imports: [TuiButton, TuiPdfViewer, TuiResponsiveDialog, TuiTitle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
private readonly sanitizer = inject(DomSanitizer);
protected readonly alerts = inject(TuiNotificationService);
protected readonly isMobile = inject(WA_IS_MOBILE);
protected readonly pdf = '/assets/media/taiga.pdf';
protected open = false;
protected readonly url = this.sanitizer.bypassSecurityTrustResourceUrl(
this.isMobile
? `https://drive.google.com/viewerng/viewer?embedded=true&url=https://taiga-ui.dev/${this.pdf}`
: this.pdf,
);
}
```
#### Loading and error states
Use Loader and BlockStatus to display additional states
**Template:**
```html
Open
file.pdf @if (loading()) { } @else if (!error()) { } @else {
Something went wrong
Try again later
Retry
}
```
**TypeScript:**
```ts
import {Component, inject, signal} from '@angular/core';
import {DomSanitizer} from '@angular/platform-browser';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_MOBILE} from '@ng-web-apis/platform';
import {TuiButton, TuiDialog, TuiLoader, TuiTitle} from '@taiga-ui/core';
import {TuiBlockStatus, TuiPdfViewer} from '@taiga-ui/layout';
@Component({
imports: [TuiBlockStatus, TuiButton, TuiDialog, TuiLoader, TuiPdfViewer, TuiTitle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
private readonly sanitizer = inject(DomSanitizer);
protected readonly isMobile = inject(WA_IS_MOBILE);
protected readonly pdf = '/assets/media/taiga.pdf';
protected open = false;
protected readonly url = this.sanitizer.bypassSecurityTrustResourceUrl(
'https://app.embedpdf.com/',
);
protected readonly loading = signal(true);
protected readonly error = signal(false);
protected openPdf(): void {
this.open = true;
this.load();
}
protected load(): void {
this.loading.set(true);
setTimeout(() => {
this.loading.set(false);
this.error.set(Math.random() <= 0.5);
}, 1000);
}
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly routes = DemoRoute;
protected examples = ['Basic', 'With responsive dialog', 'Loading and error states'];
}
```
---
# components/PieChart
- **Package**: `ADDON-CHARTS`
- **Type**: components
- **Version**: 5.0.0
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [(activeItemIndex)] | `number` | selected fragment index |
| [size] | `TuiSizeS \| TuiSizeXL` | — |
| [value] | `readonly number[]` | — |
| [tuiHintContent] | `PolymorpheusContent>` | content of the hint bubble |
### Usage Examples
#### Sizes
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPieChart} from '@taiga-ui/addon-charts';
@Component({
imports: [TuiPieChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value = [40, 30, 20, 10];
protected index = 1;
}
```
**LESS:**
```less
.wrapper {
display: flex;
align-items: center;
}
```
#### With labels
Use `ChartHint` directive to enable hints with `tuiHintContent`
**Template:**
```html
{{ value[index] || 0 | tuiAmount: 'RUB' }}
{{ labels[index] }}
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiChartHint, TuiPieChart} from '@taiga-ui/addon-charts';
import {TuiAmountPipe} from '@taiga-ui/addon-commerce';
import {TuiHint} from '@taiga-ui/core';
@Component({
imports: [TuiAmountPipe, TuiChartHint, TuiHint, TuiPieChart],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value = [13769, 12367, 10172, 3018, 2592];
protected readonly labels = ['Food', 'Cafe', 'Open Source', 'Taxi', 'Other'];
}
```
**LESS:**
```less
:host {
--tui-chart-categorical-00: #c779d0;
--tui-chart-categorical-01: #feac5e;
--tui-chart-categorical-02: #ff5f6d;
--tui-chart-categorical-03: #4bc0c8;
--tui-chart-categorical-04: #9795cd;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {TuiDocHint} from '@demo/components/hint';
import {changeDetection} from '@demo/emulate/change-detection';
import {DemoRoute} from '@demo/routes';
import {TuiDemo} from '@demo/utils';
import {TuiChartHint, TuiPieChart} from '@taiga-ui/addon-charts';
import {TuiCurrency, tuiGetCurrencySymbol} from '@taiga-ui/addon-commerce';
import {type TuiContext, tuiRound, tuiSum} from '@taiga-ui/cdk';
import {type TuiSizeXL, type TuiSizeXS} from '@taiga-ui/core';
import {tuiFormatNumber} from '@taiga-ui/kit';
import {type PolymorpheusContent} from '@taiga-ui/polymorpheus';
@Component({
imports: [TuiChartHint, TuiDemo, TuiDocHint, TuiPieChart],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected readonly examples = ['Sizes', 'With labels'];
protected readonly routes = DemoRoute;
protected readonly valueVariants = [
[0, 30, 20, 10],
[40, 30, 20, 10],
[13769, 12367, 10172, 3018, 2592],
];
protected value = this.valueVariants[0]!;
protected readonly activeItemIndexVariants = [Number.NaN, 0, 1, 2];
protected activeItemIndex = this.activeItemIndexVariants[0]!;
protected readonly sizeVariants: ReadonlyArray = [
'xs',
's',
'm',
'l',
'xl',
];
protected size = this.sizeVariants[2]!;
protected readonly contentVariants: ReadonlyArray<
PolymorpheusContent>
> = [
'',
({$implicit}) => this.getPercent($implicit),
({$implicit}) => this.format($implicit),
];
protected hintContent = this.contentVariants[0]!;
protected getPercent(index: number): string {
return `${tuiRound((100 * (this.value[index] ?? 0)) / tuiSum(...this.value), 2)} %`;
}
protected format(index: number): string {
return `${tuiFormatNumber(this.value[index] ?? 0)} ${tuiGetCurrencySymbol(
TuiCurrency.Ruble,
)}`;
}
}
```
### LESS
```less
.chart {
margin: 0 auto;
}
```
---
# components/Pin
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
Pins are used to show a location on a 2D plane. Use `color` , `background` , `border` and `box-shadow` to customize the pin Pins are designed to be absolutely positioned on map or similar medium, therefore specifically their center is placed where you put them.
### Usage Examples
#### Default
**Template:**
```html
4.5
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiIcon} from '@taiga-ui/core';
import {TuiBadge, TuiPin} from '@taiga-ui/kit';
@Component({
imports: [TuiBadge, TuiIcon, TuiPin],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
gap: 2rem;
background: var(--tui-background-base-alt);
box-shadow: 0 0 0 2rem var(--tui-background-base-alt);
padding: 1rem 0 0 1rem;
margin-block-end: -1rem;
}
.white {
color: var(--tui-background-accent-2);
}
```
#### Dot
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPin} from '@taiga-ui/kit';
@Component({
imports: [TuiPin],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
gap: 2rem;
background: var(--tui-background-base-alt);
box-shadow: 0 0 0 2rem var(--tui-background-base-alt);
padding: 0.375rem 0 0 0.375rem;
margin-block-end: -0.375rem;
}
.blue {
background: #428bf9;
}
.yellow {
background: #ffdd2d;
}
.green {
background: #00b92d;
}
.red {
background: #f52222;
}
```
#### Openable
**Template:**
```html
4.5
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiIcon} from '@taiga-ui/core';
import {TuiBadge, TuiPin} from '@taiga-ui/kit';
@Component({
imports: [TuiBadge, TuiIcon, TuiPin],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected a = true;
protected b = false;
protected c = true;
protected d = false;
}
```
**LESS:**
```less
:host {
position: relative;
display: block;
block-size: 5rem;
border-inline-start: 2rem solid transparent;
background: var(--tui-background-base-alt);
box-shadow: 0 0 0 2rem var(--tui-background-base-alt);
}
button[tuiPin] {
position: absolute;
inset-block-start: 4rem;
}
.link {
color: var(--tui-text-action);
background: var(--tui-background-elevation-1);
}
```
#### Label
**Template:**
```html
Title
Title Subtitle
Very very long title that gets cut
4.5
Title
Subtitle with an icon
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiIcon, TuiTitle} from '@taiga-ui/core';
import {TuiBadge, TuiFade, TuiPin} from '@taiga-ui/kit';
@Component({
imports: [TuiBadge, TuiFade, TuiIcon, TuiPin, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected a = true;
protected b = false;
protected c = false;
protected d = false;
}
```
**LESS:**
```less
:host {
display: grid;
gap: 1rem;
grid-auto-rows: 2rem;
place-items: start;
padding: 3rem 6rem 0;
background-color: var(--tui-background-base-alt);
box-shadow: 0 0 0 2rem var(--tui-background-base-alt);
}
.link {
color: var(--tui-text-action);
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = ['Default', 'Dot', 'Openable', 'Label'];
}
```
---
# components/Pincode
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.9.0
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| type | `string` | masks each digit as a filled dot |
| [invalid] | `boolean \| null` | — shake and auto-clear |
| maxLength | `number` | number of cells, pending animation triggers once value reaches this length |
### API - Outputs
| Event | Type | Description |
|-------|------|-------------|
| (confirmed) | `void` | fires after the success wave, before dots collapse. Use to reveal a loader for the next-step transition |
| (finished) | `void` | fires at the end of the verification cycle — after success collapse or after invalid shake + auto-clear |
### Usage Examples
#### Basic
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPincode} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiPincode],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected value = '';
}
```
#### Dots
Masked mode via `type="password"` — entered digits render as filled dots
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPincode} from '@taiga-ui/kit';
@Component({
imports: [FormsModule, TuiPincode],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected value = '';
}
```
#### Fast loading
Fast loading — response under 500ms. Enter `1234` for success, any other code for error
**Template:**
```html
@if (done()) { PIN verified ✓
} @else {
}
```
**TypeScript:**
```ts
import {Component, resource, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiPincode} from '@taiga-ui/kit';
const CORRECT = '1234';
const FAST_LATENCY = 300;
async function fakeApiInvalidate(pin: string): Promise {
return new Promise((resolve) =>
setTimeout(() => resolve(pin !== CORRECT), FAST_LATENCY),
);
}
@Component({
imports: [FormsModule, TuiPincode],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly verification = resource({
request: () => this.pin(),
loader: async ({request}) =>
request.length === 4 ? fakeApiInvalidate(request) : null,
});
protected readonly done = signal(false);
protected readonly pin = signal('');
}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 1.5rem;
}
```
#### Long loading
Long loading — response over 700ms, auto-bounce while waiting. Enter `1234` for success, any other code for error
**Template:**
```html
@if (done()) { PIN verified ✓
} @else {
}
```
**TypeScript:**
```ts
import {Component, resource, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiLoader} from '@taiga-ui/core';
import {TuiPincode} from '@taiga-ui/kit';
const CORRECT = '1234';
async function fakeApiInvalidate(pin: string): Promise {
return new Promise((resolve) => setTimeout(() => resolve(pin !== CORRECT), 1000));
}
@Component({
imports: [FormsModule, TuiLoader, TuiPincode],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly verification = resource({
request: () => this.pin(),
loader: async ({request}) =>
request.length === 4 ? fakeApiInvalidate(request) : null,
});
protected readonly done = signal(false);
protected readonly pin = signal('');
protected onFinished(): void {
if (this.verification.value() === false) {
setTimeout(() => this.done.set(true), 2000);
}
}
}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.5rem;
padding: 1.5rem;
}
.done {
color: var(--tui-text-positive);
font-size: 1rem;
font-weight: 500;
}
.field {
position: relative;
display: flex;
align-items: center;
justify-content: center;
block-size: 4rem;
}
tui-loader {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--tui-text-action);
scale: 0;
pointer-events: none;
}
.field_success tui-loader {
animation: loaderIn 500ms cubic-bezier(0, 0, 0.58, 1) 2150ms forwards;
}
@keyframes loaderIn {
0% {
scale: 0;
}
30% {
scale: 1.2;
}
100% {
scale: 1;
}
}
```
#### Desktop
Desktop dialog with auto-focused input and full verification flow. Same logic as Mobile, native keyboard input. Enter `1234` for successful validation
**Template:**
```html
Open dialog
```
**TypeScript:**
```ts
import {Component, resource, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiThumbnailCard} from '@taiga-ui/addon-commerce';
import {TuiAutoFocus} from '@taiga-ui/cdk';
import {TuiButton, TuiDialog, TuiTitle} from '@taiga-ui/core';
import {TuiPincode} from '@taiga-ui/kit';
import {TuiHeader} from '@taiga-ui/layout';
const CORRECT = '1234';
async function fakeApiInvalidate(pin: string): Promise {
return new Promise((resolve) => setTimeout(() => resolve(pin !== CORRECT), 1000));
}
@Component({
imports: [
FormsModule,
TuiAutoFocus,
TuiButton,
TuiDialog,
TuiHeader,
TuiPincode,
TuiThumbnailCard,
TuiTitle,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly verification = resource({
request: () => this.pin(),
loader: async ({request}) =>
request.length === 4 ? fakeApiInvalidate(request) : null,
});
protected readonly open = signal(false);
protected readonly pin = signal('');
}
```
**LESS:**
```less
.content {
display: flex;
flex-direction: column;
align-items: center;
gap: 1.25rem;
padding: 1.5rem 1.5rem 0;
}
.heading {
justify-content: center;
}
.container {
display: flex;
align-items: center;
justify-content: center;
block-size: 4rem;
}
.footer {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 0.5rem;
padding-block-start: 0.5rem;
}
```
#### Mobile
Mobile bottom sheet with a custom numeric pad and full verification flow — `inputmode="none"` suppresses the native keyboard. Enter `1234` for successful validation; wrong code triggers shake-clear-retry
**Template:**
```html
Open sheet
```
**TypeScript:**
```ts
import {Component, resource, signal} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiThumbnailCard} from '@taiga-ui/addon-commerce';
import {TuiSheetDialog, type TuiSheetDialogOptions} from '@taiga-ui/addon-mobile';
import {TuiButton, TuiIcon, TuiLink, TuiTitle} from '@taiga-ui/core';
import {TuiPincode} from '@taiga-ui/kit';
import {TuiAppBar, TuiHeader} from '@taiga-ui/layout';
const CORRECT = '1234';
async function fakeApiInvalidate(pin: string): Promise {
return new Promise((resolve) => setTimeout(() => resolve(pin !== CORRECT), 1000));
}
@Component({
imports: [
FormsModule,
TuiAppBar,
TuiButton,
TuiHeader,
TuiIcon,
TuiLink,
TuiPincode,
TuiSheetDialog,
TuiThumbnailCard,
TuiTitle,
],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly verification = resource({
request: () => this.pin(),
loader: async ({request}) =>
request.length === 4 ? fakeApiInvalidate(request) : null,
});
protected readonly open = signal(false);
protected readonly pin = signal('');
protected readonly keys = [
'1',
'2',
'3',
'4',
'5',
'6',
'7',
'8',
'9',
'',
'0',
'backspace',
] as const;
protected readonly options: Partial = {
closable: false,
bar: false,
appearance: 'fullscreen',
};
protected onKey(key: string): void {
if (this.verification.value() != null) {
return;
}
const value = this.pin();
if (key === 'backspace') {
this.pin.set(value.slice(0, -1));
return;
}
if (value.length >= 4) {
return;
}
this.pin.set(`${value}${key}`);
}
}
```
**LESS:**
```less
.header {
margin-block-end: 1.5rem;
}
.content {
display: flex;
flex-direction: column;
align-items: center;
flex-grow: 1;
}
.container {
display: flex;
align-items: center;
justify-content: center;
block-size: 4rem;
margin-block-end: auto;
}
.pad {
display: grid;
grid-template-columns: repeat(3, 4.5rem);
gap: 0.5rem 1.5rem;
justify-items: center;
margin-block-end: 0.5rem;
}
.key {
display: flex;
align-items: center;
justify-content: center;
inline-size: 4.5rem;
block-size: 4.5rem;
border: none;
border-radius: 50%;
background: transparent;
font: 2.5rem/1 var(--tui-typography-family-display);
color: var(--tui-text-primary);
cursor: pointer;
transition: background var(--tui-duration-short, 150ms);
@media (pointer: fine) {
&:hover {
background: var(--tui-background-neutral-1-hover);
}
}
&:focus-visible {
outline: 0.125rem solid var(--tui-border-focus);
outline-offset: 0.125rem;
}
&:active {
background: var(--tui-background-neutral-1-pressed);
}
&:disabled {
opacity: 0.4;
cursor: default;
}
}
tui-app-bar {
align-self: stretch;
}
```
### TypeScript
```ts
import {Component, signal} from '@angular/core';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiPincode} from '@taiga-ui/kit';
@Component({
imports: [ReactiveFormsModule, TuiDemo, TuiPincode],
templateUrl: './index.html',
changeDetection,
})
export default class PageComponent {
protected readonly examples = [
'Basic',
'Dots',
'Fast loading',
'Long loading',
'Desktop',
'Mobile',
];
protected length = 4;
protected readonly invalidVariants = [true, false] as const;
protected readonly typeVariants = ['text', 'password'] as const;
protected readonly invalid = signal(null);
protected readonly type = signal<'password' | 'text'>('text');
protected readonly control = new FormControl('');
protected onFinished(): void {
setTimeout(() => {
this.invalid.set(null);
this.control.reset('');
}, 1500);
}
}
```
---
# components/Popout
- **Package**: `EXPERIMENTAL`
- **Type**: components
- **Version**: 5.0.0
Using service to show content in a new window.
### Usage Examples
#### Popout window
**Template:**
```html
Bidirectional communication
Toggle popout window
```
**TypeScript:**
```ts
import {Component, inject, signal, type TemplateRef} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiAutoFocus} from '@taiga-ui/cdk';
import {TuiButton, TuiInput} from '@taiga-ui/core';
import {TuiPopoutService} from '@taiga-ui/experimental';
import {TuiCard} from '@taiga-ui/layout';
import {type Subscription} from 'rxjs';
@Component({
imports: [FormsModule, TuiAutoFocus, TuiButton, TuiCard, TuiInput],
templateUrl: './index.html',
changeDetection,
})
export default class Example {
private readonly popout = inject(TuiPopoutService);
private sub: Subscription | null = null;
protected readonly value = signal('Value');
protected toggle(content: TemplateRef): void {
if (this.sub) {
this.sub.unsubscribe();
this.sub = null;
} else {
this.sub = this.popout
.open(content, {
title: 'Page',
features: {
width: 320,
height: 160,
left: 600,
top: 300,
},
})
.subscribe({
complete: () => {
this.sub = null;
},
});
}
}
}
```
#### Picture in Picture
**Template:**
```html
@if (!supported) { Document Picture-in-Picture is not supported in your browser, fallback to regular popout window will be used
} Open Picture-in-Picture
```
**TypeScript:**
```ts
import {Component, inject, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {WA_DOCUMENT_PIP} from '@ng-web-apis/experimental';
import {TuiButton, TuiNotification} from '@taiga-ui/core';
import {TuiPopoutService} from '@taiga-ui/experimental';
import {PolymorpheusComponent} from '@taiga-ui/polymorpheus';
import {type Subscription} from 'rxjs';
import {Popout} from './popout';
@Component({
imports: [TuiButton, TuiNotification],
templateUrl: './index.html',
changeDetection,
})
export default class Example {
protected readonly popout = inject(TuiPopoutService);
protected readonly supported = !!inject(WA_DOCUMENT_PIP)?.requestWindow;
protected readonly subscription = signal(null);
protected open(): void {
this.subscription.set(
this.popout
.open(new PolymorpheusComponent(Popout), {
pip: true,
features: {height: 480, width: 320},
})
.subscribe({complete: () => this.subscription.set(null)}),
);
}
}
```
#### Directive
**Template:**
```html
Call us
Taiga Headquarters
Calling...
```
**TypeScript:**
```ts
import {Component, signal} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiButton, TuiTitle} from '@taiga-ui/core';
import {TuiPopout} from '@taiga-ui/experimental';
import {TuiAvatar} from '@taiga-ui/kit';
import {TuiHeader} from '@taiga-ui/layout';
@Component({
imports: [TuiAvatar, TuiButton, TuiHeader, TuiPopout, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Example {
protected readonly open = signal(false);
}
```
**LESS:**
```less
main {
display: grid;
block-size: 100vh;
place-items: center;
place-content: center;
grid-auto-rows: min-content;
gap: 2rem;
padding: 2rem;
box-sizing: border-box;
background: var(--tui-background-base);
}
section {
display: flex;
gap: 1rem;
}
[tuiTitle] {
text-align: center;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page extends Array {
protected readonly examples = ['Popout window', 'Picture in Picture', 'Directive'];
protected readonly [1] = {
'popout.ts': import('./examples/2/popout.ts?raw', {with: {loader: 'text'}}),
};
protected readonly exampleService = import('./examples/import/service.md');
}
```
---
# components/Preview
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
Preview component allows to open modal for viewing some document and to work with it (download, zoom, rotate etc) As a document you can provide images, embeds and other arbitrary content. The component automatically adjusts to the mobile device
### Usage Examples
#### Full preview
**Template:**
```html
With all features
Show preview
{{ titles[index] }}
Delete
Download
Close
Important document
Hello everyone! This is some important document in A4 format, although it is made using html
This shows that the component preview can work with absolutely any content: this way you can show any template, image, pdf or even iframe with your favorite site. We will put this content in the center of the portal and provide the user with control over it, and we will provide you with convenient levers to change it and process actions.
```
**TypeScript:**
```ts
import {Component, inject, type TemplateRef, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {tuiClamp, TuiSwipe, type TuiSwipeEvent} from '@taiga-ui/cdk';
import {TuiButton, type TuiDialogContext, TuiNotificationService} from '@taiga-ui/core';
import {TuiPreview, TuiPreviewDialogService} from '@taiga-ui/kit';
import {type PolymorpheusContent, PolymorpheusOutlet} from '@taiga-ui/polymorpheus';
@Component({
imports: [PolymorpheusOutlet, TuiButton, TuiPreview, TuiSwipe],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly previewService = inject(TuiPreviewDialogService);
private readonly alerts = inject(TuiNotificationService);
protected readonly preview = viewChild>('preview');
protected readonly contentSample =
viewChild>>('contentSample');
protected index = 0;
protected length = 2;
protected titles = ['Transaction cert.jpg', 'My face.jpg'];
protected get previewContent(): PolymorpheusContent {
const content = this.contentSample();
return this.index === 0 && content
? content
: 'https://avatars.githubusercontent.com/u/10106368';
}
protected show(): void {
this.previewService.open(this.preview() || '').subscribe({
complete: () => console.info('complete'),
});
}
protected download(): void {
this.alerts.open('Downloading...').subscribe();
}
protected delete(): void {
this.alerts.open('Deleting...').subscribe();
}
protected onSwipe(swipe: TuiSwipeEvent): void {
if (swipe.direction === 'left') {
this.index = tuiClamp(this.index + 1, 0, this.length - 1);
}
if (swipe.direction === 'right') {
this.index = tuiClamp(this.index - 1, 0, this.length - 1);
}
}
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
.content {
font: var(--tui-typography-body-l);
background-color: var(--tui-background-base);
inline-size: 50rem;
block-size: 68.75rem;
padding: 3.75rem;
box-sizing: border-box;
border-radius: 0.75rem;
}
.polymorpheus {
padding: 2.5rem 10.375rem;
}
```
#### Preview with directive
**Template:**
```html
Show preview
{{ titles[index] }}
Close
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton} from '@taiga-ui/core';
import {TuiPreview} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiPreview],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected open = false;
protected index = 0;
protected length = 2;
protected titles = ['pic_1.jpg', 'pic_2.jpg'];
protected content = [
'https://picsum.photos/600/500',
'https://picsum.photos/500/600',
];
}
```
#### Simple mode
**Template:**
```html
Show simple preview
VIDEO
Close
```
**TypeScript:**
```ts
import {Component, inject, type TemplateRef, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiButton, type TuiDialogContext} from '@taiga-ui/core';
import {TuiPreview, TuiPreviewDialogService} from '@taiga-ui/kit';
@Component({
imports: [TuiButton, TuiPreview],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly previewDialogService = inject(TuiPreviewDialogService);
protected readonly preview = viewChild>('preview');
protected show(): void {
this.previewDialogService.open(this.preview() || '').subscribe();
}
}
```
**LESS:**
```less
.content {
inline-size: 80%;
block-size: 80%;
}
```
#### With loading and unavailable image
**Template:**
```html
Show preview
{{ title$ | async }}
Download
Close @if (contentUnavailable$ | async) { } @if (imageSrc$ | async; as src) { } @if (loading$ | async) { }
```
**TypeScript:**
```ts
import {AsyncPipe} from '@angular/common';
import {Component, inject, type TemplateRef, viewChild} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {tuiIsPresent} from '@taiga-ui/cdk';
import {TuiButton, type TuiDialogContext, TuiIcon, TuiLoader} from '@taiga-ui/core';
import {TuiPreview, TuiPreviewDialogService} from '@taiga-ui/kit';
import {
BehaviorSubject,
filter,
map,
type Observable,
of,
startWith,
switchMap,
timer,
} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiButton, TuiIcon, TuiLoader, TuiPreview],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly previewDialogService = inject(TuiPreviewDialogService);
protected readonly preview = viewChild>('preview');
protected readonly items = [
{
title: 'some table.xlsx',
hasPreview: false,
},
{
title: 'Content #2',
hasPreview: true,
},
];
protected readonly index$$ = new BehaviorSubject(0);
protected readonly item$ = this.index$$.pipe(
map((index) => this.items[index]),
filter(tuiIsPresent),
);
protected readonly title$ = this.item$.pipe(map((item) => item.title));
protected readonly contentUnavailable$ = this.item$.pipe(
map((item) => !item.hasPreview),
);
protected readonly imageSrc$ = this.item$.pipe(
switchMap((item) =>
item.hasPreview ? this.emulateBackendRequest().pipe(startWith('')) : of(null),
),
);
protected readonly loading$ = this.imageSrc$.pipe(map((src) => src === ''));
protected show(): void {
this.previewDialogService.open(this.preview() || '').subscribe();
}
protected download(): void {
console.info('downloading...');
}
protected emulateBackendRequest(): Observable {
return timer(1500).pipe(
map(() => 'https://ng-web-apis.github.io/dist/assets/images/web-api.svg'),
);
}
}
```
**LESS:**
```less
.content {
background-color: rgb(245, 241, 241);
inline-size: 25rem;
block-size: 37.5rem;
padding: 2.5rem;
border-radius: 0.75rem;
}
.t-container {
display: flex;
flex-direction: column;
align-items: center;
color: var(--tui-text-secondary);
}
.t-icon {
margin-block-end: 0.75rem;
font-size: 5rem;
}
.t-loader {
inline-size: 4rem;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
@Component({
imports: [TuiDemo],
templateUrl: './index.html',
changeDetection,
})
export default class Page {
protected readonly examples = [
'Full preview',
'Preview with directive',
'Simple mode',
'With loading and unavailable image',
];
}
```
---
# components/ProgressBar
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
`tuiProgressBar` – attribute component for native html tag `'">` . Usage: `'">` .
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [value] | `number` | |
| [max] | `number` | |
| [size] | `TuiSizeXS \| TuiSizeXXL` | — |
| [color] | `string` | |
### Usage Examples
#### Basic
**Template:**
```html
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {map, of, startWith, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(40)
: timer(300, 300).pipe(
map((i) => i + 30),
startWith(30),
);
}
```
#### Multicolor
**Template:**
```html
Single color
Use '"> 's CSS-property color to set solid color of progress indicator.
With fancy color gradient
Set component's input property color to get more complex color combinations.
Use directive tuiProgressFixedGradient to make gradient fixed.
Multicolor segments
Use tuiProgressColorSegments directive to to get multicolor segments.
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {of, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
private readonly animationDisabled =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID));
protected readonly fastValue$ = this.animationDisabled ? of(80) : timer(500, 100);
protected readonly slowValue$ = this.animationDisabled ? of(4) : timer(500, 2000);
protected readonly colors = [
'var(--tui-chart-categorical-01)',
'var(--tui-chart-categorical-21)',
'lightskyblue',
'#3682db',
'var(--tui-background-accent-1)',
];
}
```
**LESS:**
```less
.progress {
margin-block-end: 1rem;
color: var(--tui-chart-categorical-09);
}
.description {
font: var(--tui-typography-heading-h6);
margin-block-end: 1rem;
&:first-child {
margin-block-start: 0;
}
}
```
#### Sizes
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
flex-direction: column;
gap: 1rem;
}
```
#### With label
**Template:**
```html
@if (value$ | async; as value) { {{ value }}%
}
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {map, of, startWith, takeWhile, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly max = 100;
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(30)
: timer(300, 300).pipe(
map((i) => i + 30),
startWith(30),
takeWhile((value) => value <= this.max),
);
}
```
**LESS:**
```less
.label-wrapper {
inline-size: 100%;
text-shadow: 0 0 0.25rem #000;
color: var(--tui-text-primary-on-accent-1);
}
```
#### Stacked progress bars
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
.label-wrapper {
inline-size: 100%;
}
.progress {
&:nth-child(1) {
color: #a3ecb3;
}
&:nth-child(2) {
color: #39b54a;
}
}
```
#### Indeterminate
Indeterminate state expresses an unspecified amount of wait time. If there is no `[value]` attribute, the `ProgressBar` is indeterminate.
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {}
```
#### Customizable corners
You can toggle off round corners of the progress bar by setting `border-radius: 0` .
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
[tuiProgressBar] {
border-radius: 0;
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiLink, type TuiSizeXS, type TuiSizeXXL} from '@taiga-ui/core';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiLink, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
})
export default class Page {
protected value = 6;
protected max = 10;
protected readonly sizeVariants: ReadonlyArray = [
'xs',
's',
'm',
'l',
'xl',
'xxl',
];
protected size = this.sizeVariants[2]!;
protected readonly colorVariants: readonly string[] = [
'var(--tui-background-accent-1)',
'lightskyblue',
'#3682db',
'rgba(74, 201, 155, 1)',
'linear-gradient(to right, var(--tui-chart-categorical-02), var(--tui-chart-categorical-14), var(--tui-chart-categorical-12))',
];
protected color = this.colorVariants[0]!;
protected readonly examples = [
'Basic',
'Multicolor',
'Sizes',
'With label',
'Stacked progress bars',
'Indeterminate',
'Customizable corners',
];
}
```
### LESS
```less
@import '@taiga-ui/styles/utils';
.api-progress {
inline-size: 50%;
@media @tui-mobile {
inline-size: 100%;
}
}
dt,
dd {
display: inline;
margin: 0;
}
```
---
# components/ProgressCircle
- **Package**: `KIT`
- **Type**: components
- **Version**: 5.0.0
`'">` is a component to visually represent the completion of a process or operation (as a partially filled circle/ring). It shows how much has been completed and how much remains.
### Example
```html
```
### API - Inputs
| Property | Type | Description |
|----------|-----|----------|
| [value] | `number` | is omitted. |
| [max] | `number` | maximum value of the progress |
| [size] | `TuiSizeXXL \| TuiSizeXXS` | — |
| [color] | `string` | |
| [arc] | `boolean` | use arc shape with small bottom open arc sector (instead of default circle). |
| [style.--tui-thickness.px] | `number` | width of the circle's stroke |
### Usage Examples
#### Basic
**Template:**
```html
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {map, of, startWith, takeWhile, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly max = 100;
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(30)
: timer(300, 200).pipe(
map((i) => i + 30),
startWith(30),
takeWhile((value) => value <= this.max),
);
}
```
#### Sizes
**Template:**
```html
@for (size of sizes; track size) { }
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly sizes = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl'] as const;
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
display: flex;
flex-direction: column;
gap: 1rem;
@media @tui-desktop-min {
flex-direction: row-reverse;
}
}
```
#### With label
**Template:**
```html
@if (value$ | async; as value) {
Done
{{ value }}%
}
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiTitle} from '@taiga-ui/core';
import {TuiProgress} from '@taiga-ui/kit';
import {TuiHeader} from '@taiga-ui/layout';
import {map, of, startWith, takeWhile, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiHeader, TuiProgress, TuiTitle],
templateUrl: './index.html',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly max = 100;
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(30)
: timer(300, 200).pipe(
map((i) => i + 30),
startWith(30),
takeWhile((value) => value <= this.max),
);
}
```
#### Colors
**Template:**
```html
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
:host {
display: flex;
flex-wrap: wrap;
gap: 1rem;
}
.progress {
&[data-size='l'] {
color: var(--tui-chart-categorical-01);
}
&[data-size='m'] {
color: var(--tui-chart-categorical-03);
}
&[data-size='s'] {
color: var(--tui-chart-categorical-09);
}
}
```
#### Dynamic color
**Template:**
```html
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {map, of, repeat, share, takeWhile, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly max = 100;
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(30)
: timer(300, 200).pipe(
takeWhile((value) => value <= this.max),
share(),
repeat(),
);
protected readonly color$ = this.value$.pipe(
map((value) => {
if (value < 33) {
return 'red';
}
return value < 66 ? 'yellow' : 'green';
}),
);
}
```
**LESS:**
```less
tui-progress-circle {
transition: color 2s;
}
```
#### Anti-clockwise direction
Use power of CSS property `transform` to customize direction and starting point of progress circle.
**Template:**
```html
```
**TypeScript:**
```ts
import {AsyncPipe, isPlatformServer} from '@angular/common';
import {Component, inject, PLATFORM_ID} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_E2E} from '@ng-web-apis/platform';
import {TuiProgress} from '@taiga-ui/kit';
import {of, repeat, takeWhile, timer} from 'rxjs';
@Component({
imports: [AsyncPipe, TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly max = 100;
protected readonly value$ =
inject(WA_IS_E2E) || isPlatformServer(inject(PLATFORM_ID))
? of(30)
: timer(300, 200).pipe(
takeWhile((value) => value <= this.max),
repeat(),
);
}
```
**LESS:**
```less
tui-progress-circle {
transform: scaleX(-1);
}
```
#### Thickness
Use css-variable `--tui-thickness` to customize width of the circle's stroke.
**Template:**
```html
@for (_ of '-'.repeat(4); track $index) { }
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
tui-progress-circle {
&:nth-child(1) {
--tui-thickness: 0.125rem;
}
&:nth-child(2) {
--tui-thickness: 0.25rem;
}
&:nth-child(3) {
--tui-thickness: 0.375rem;
}
&:nth-child(4) {
--tui-thickness: 0.5rem;
}
}
:host {
display: flex;
flex-direction: column;
gap: 1rem;
@media @tui-desktop-min {
flex-direction: row;
}
}
```
#### Arc mode
Set `arc` attribute to transform default circular shape into arc with small bottom open arc sector (gap between arc ends).
**Template:**
```html
@for (size of sizes; track size) { }
```
**TypeScript:**
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiProgress],
templateUrl: './index.html',
styleUrl: './index.less',
encapsulation,
changeDetection,
})
export default class Example {
protected readonly sizes = ['xxs', 'xs', 's', 'm', 'l', 'xl', 'xxl'] as const;
}
```
**LESS:**
```less
@import '@taiga-ui/styles/utils';
:host {
display: flex;
flex-direction: row-reverse;
gap: 1rem;
@media @tui-mobile {
flex-direction: column;
margin-block-end: 1rem;
}
}
```
### TypeScript
```ts
import {Component} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {TuiDemo} from '@demo/utils';
import {TuiLink, type TuiSizeXXL, type TuiSizeXXS, TuiTitle} from '@taiga-ui/core';
import {tuiInputNumberOptionsProvider, TuiProgress} from '@taiga-ui/kit';
@Component({
imports: [TuiDemo, TuiLink, TuiProgress, TuiTitle],
templateUrl: './index.html',
styleUrl: './index.less',
changeDetection,
providers: [tuiInputNumberOptionsProvider({min: 0})],
})
export default class Page {
protected value = 6;
protected max = 10;
protected arc = false;
protected thickness = 6;
protected readonly sizeVariants: ReadonlyArray = [
'xxs',
'xs',
's',
'm',
'l',
'xl',
'xxl',
];
protected size: TuiSizeXXL | TuiSizeXXS = 'm';
protected readonly colorVariants: readonly string[] = [
'var(--tui-background-accent-1)',
'lightskyblue',
'#3682db',
'rgba(74, 201, 155, 1)',
'url(#gradient)',
];
protected color = this.colorVariants[0]!;
protected readonly examples = [
'Basic',
'Sizes',
'With label',
'Colors',
'Dynamic color',
'Anti-clockwise direction',
'Thickness',
'Arc mode',
];
}
```
### LESS
```less
dt,
dd {
display: inline;
margin: 0;
}
```
---
# components/PullToRefresh
- **Package**: `ADDON-MOBILE`
- **Type**: components
- **Version**: 5.0.0
Component to refresh content after pull top. It emulates appearance of native iOS and Android components It emits `(pulled)` event when the pull threshold is reached. You can set that threshold in pixels by `TUI_PULL_TO_REFRESH_THRESHOLD` DI token. You can finish loading with `TUI_PULL_TO_REFRESH_LOADED` stream token that can be provided in DI. While the scroll container is scrolled to the top, the component automatically applies `overscroll-behavior: none;` to it so elastic scrolling on iOS does not interfere with pulling. Once scrolled away from the top, the property is removed and the native bounce works as usual
### Usage Examples
#### Android
**Template:**
```html
Finish loading
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_ANDROID, WA_IS_IOS} from '@ng-web-apis/platform';
import {
TUI_ANDROID_LOADER,
TUI_PULL_TO_REFRESH_COMPONENT,
TUI_PULL_TO_REFRESH_LOADED,
TuiPullToRefresh,
} from '@taiga-ui/addon-mobile';
import {TuiButton, TuiNotificationService} from '@taiga-ui/core';
import {Subject} from 'rxjs';
@Component({
selector: 'example-1',
imports: [TuiButton, TuiPullToRefresh],
templateUrl: './index.html',
encapsulation,
changeDetection,
providers: [
{
provide: WA_IS_IOS,
useValue: false,
},
{
provide: WA_IS_ANDROID,
useValue: true,
},
{
provide: TUI_PULL_TO_REFRESH_COMPONENT,
useValue: TUI_ANDROID_LOADER,
},
{
provide: TUI_PULL_TO_REFRESH_LOADED,
useClass: Subject,
},
],
})
export default class Example {
private readonly alerts = inject(TuiNotificationService);
private readonly loaded$ = inject>(TUI_PULL_TO_REFRESH_LOADED);
protected onPull(): void {
this.alerts.open('Loading...').subscribe();
}
protected finishLoading(): void {
this.loaded$.next();
}
}
```
#### iOS
**Template:**
```html
Finish loading
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
```
**TypeScript:**
```ts
import {Component, inject} from '@angular/core';
import {changeDetection} from '@demo/emulate/change-detection';
import {encapsulation} from '@demo/emulate/encapsulation';
import {WA_IS_ANDROID, WA_IS_IOS} from '@ng-web-apis/platform';
import {
TUI_IOS_LOADER,
TUI_PULL_TO_REFRESH_COMPONENT,
TUI_PULL_TO_REFRESH_LOADED,
TuiPullToRefresh,
} from '@taiga-ui/addon-mobile';
import {TuiButton, TuiNotificationService} from '@taiga-ui/core';
import {Subject} from 'rxjs';
@Component({
imports: [TuiButton, TuiPullToRefresh],
templateUrl: './index.html',
encapsulation,
changeDetection,
providers: [
{
provide: WA_IS_IOS,
useValue: true,
},
{
provide: WA_IS_ANDROID,
useValue: false,
},
{
provide: TUI_PULL_TO_REFRESH_COMPONENT,
useValue: TUI_IOS_LOADER,
},
{
provide: TUI_PULL_TO_REFRESH_LOADED,
useClass: Subject,
},
],
})
export default class Example {
private readonly alerts = inject(TuiNotificationService);
private readonly loaded$ = inject>(TUI_PULL_TO_REFRESH_LOADED);
protected onPull(): void {
this.alerts.open('Loading...').subscribe();
}
protected finishLoading(): void {
this.loaded$.next();
}
}
```
#### Virtual scroll
**Template:**
```html