Taiga UI 5 is out!

Keypad ADDON-MOBILE

Examples API GitHub

On this page

TuiKeypad is an on-screen grid primitive for touch input. It only lays out and styles the keys — you project your own native <button> or <a> elements and wire clicks, labels and disabled state yourself, so the pad can be anything: a numeric pad, a shuffled PIN terminal, a calculator or a custom chooser.

Each key stays a real element, so (click) , aria-label , disabled and href all work as usual. Each key updates your own state on (click) — display the running value however you like (the examples keep it in a signal shown in a <div> ).

Basic keypad

A numeric pad — digits, clear and backspace wired via (click) .

Value: —

    
      
    
    
      <tui-keypad class="keypad">
    @for (digit of digits; track digit) {
        <button
            type="button"
            (click)="append(digit)"
        >
            {{ digit }}
        </button>
    }

    <button
        type="button"
        (click)="clear()"
    >
        C
    </button>

    <button
        type="button"
        (click)="append('0')"
    >
        0
    </button>

    <button
        aria-label="Backspace"
        type="button"
        (click)="backspace()"
        (longtap)="clear()"
    >
        <tui-icon icon="@tui.delete" />
    </button>
</tui-keypad>

<p>Value: {{ value() || '—' }}</p>

    
    
      .keypad {
    inline-size: 15rem;
}

    

Custom keys

A custom-font pad with a conditional backspace icon, driving a display.
    
      
    
    
      <div class="display">{{ value() || '—' }}</div>

<tui-keypad class="custom">
    @for (digit of digits; track digit) {
        <button
            type="button"
            (click)="append(digit)"
        >
            {{ digit }}
        </button>
    }

    <div></div>

    <button
        type="button"
        (click)="append('0')"
    >
        0
    </button>

    @if (value()) {
        <button
            aria-label="Backspace"
            type="button"
            (click)="backspace()"
            (longtap)="clear()"
        >
            <tui-icon icon="@tui.delete" />
        </button>
    }
</tui-keypad>

    
    
      :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.

    
      
    
    
      <p class="pin">{{ masked() || '—' }}</p>

<tui-keypad class="keypad">
    @for (digit of digits(); track digit) {
        <button
            type="button"
            (click)="append(digit)"
        >
            {{ digit }}
        </button>
    }

    <button
        type="button"
        (click)="clear()"
    >
        C
    </button>

    <button
        aria-label="Backspace"
        type="button"
        (click)="backspace()"
        (longtap)="clear()"
    >
        <tui-icon icon="@tui.delete" />
    </button>
</tui-keypad>

    
    
      .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.
    
      
    
    
      <button
    tuiButton
    type="button"
    (click)="open.set(true)"
>
    Open form
</button>

<ng-template
    let-observer
    [tuiSheetDialogOptions]="options"
    [(tuiSheetDialog)]="open"
>
    <div class="sheet">
        <div class="top">
            <input
                inputmode="none"
                class="amount"
                [size]="value().length + 1"
                [(ngModel)]="value"
                (blur)="focused.set(false)"
                (focus)="focused.set(true)"
            />

            <div class="chips">
                <span tuiChip>23 876 $</span>
                <span tuiChip>10 000 $</span>
                <span tuiChip>Auto-completion</span>
            </div>
        </div>

        <div class="middle">
            <div class="card">
                <div tuiCell>
                    <tui-avatar
                        appearance="primary"
                        tuiAvatar="@tui.circle-user"
                    />
                    <div tuiTitle>
                        23 876 $
                        <div tuiSubtitle>from Card 1</div>
                    </div>
                </div>
                <div tuiCell>
                    <tui-avatar
                        appearance="secondary"
                        tuiAvatar="@tui.circle-dollar-sign"
                    />
                    <div tuiTitle>
                        1 450 $
                        <div tuiSubtitle>from Card 2</div>
                    </div>
                </div>
            </div>
        </div>

        <footer tuiFloatingContainer>
            <button
                tuiButton
                type="button"
                (click)="observer.complete()"
            >
                Send
            </button>

            <div class="clamp">Commission-free</div>

            @if (focused()) {
                <div
                    tuiAnimated
                    class="pad"
                >
                    <div class="pad-inner">
                        <tui-keypad>
                            @for (digit of digits; track digit) {
                                <button
                                    type="button"
                                    (click)="append(digit)"
                                >
                                    {{ digit }}
                                </button>
                            }

                            <div></div>

                            <button
                                type="button"
                                (click)="append('0')"
                            >
                                0
                            </button>

                            <button
                                aria-label="Backspace"
                                type="button"
                                (click)="backspace()"
                                (longtap)="clear()"
                            >
                                <tui-icon icon="@tui.delete" />
                            </button>
                        </tui-keypad>
                    </div>
                </div>
            }
        </footer>
    </div>
</ng-template>

    
    
      @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 <input> 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.
    
      
    
    
      <button
    iconStart="@tui.calculator"
    tuiButton
    type="button"
    (click)="open.set(true)"
>
    Open Calculator
</button>

<ng-template
    [tuiPopout]="open()"
    [tuiPopoutOptions]="{pip: true, features: {width: 300, height: 450}}"
    (tuiPopoutChange)="open.set($event)"
>
    <main class="calculator">
        <input
            #input
            inputmode="none"
            readonly
            tuiAutoFocus
            class="input"
            [value]="displayValue()"
            (keydown)="onKeydown($event)"
        />

        <tui-keypad [columns]="4">
            <button
                type="button"
                (click)="onKey('clear')"
            >
                AC
            </button>
            <button
                type="button"
                (click)="onKey('(')"
            >
                (
            </button>
            <button
                type="button"
                (click)="onKey(')')"
            >
                )
            </button>
            <button
                aria-label="Divide"
                type="button"
                (click)="onKey('÷')"
            >
                <tui-icon icon="@tui.divide" />
            </button>

            @for (digit of ['7', '8', '9']; track digit) {
                <button
                    type="button"
                    (click)="onKey(digit)"
                >
                    {{ digit }}
                </button>
            }
            <button
                aria-label="Multiply"
                type="button"
                (click)="onKey('×')"
            >
                <tui-icon icon="@tui.x" />
            </button>

            @for (digit of ['4', '5', '6']; track digit) {
                <button
                    type="button"
                    (click)="onKey(digit)"
                >
                    {{ digit }}
                </button>
            }
            <button
                aria-label="Subtract"
                type="button"
                (click)="onKey('-')"
            >
                <tui-icon icon="@tui.minus" />
            </button>

            @for (digit of ['1', '2', '3']; track digit) {
                <button
                    type="button"
                    (click)="onKey(digit)"
                >
                    {{ digit }}
                </button>
            }
            <button
                aria-label="Add"
                type="button"
                (click)="onKey('+')"
            >
                <tui-icon icon="@tui.plus" />
            </button>

            <button
                type="button"
                (click)="onKey('0')"
            >
                0
            </button>
            <button
                type="button"
                (click)="onKey('.')"
            >
                .
            </button>
            <button
                aria-label="Backspace"
                type="button"
                (click)="onKey('backspace')"
                (longtap)="onKey('clear')"
            >
                <tui-icon icon="@tui.delete" />
            </button>
            <button
                aria-label="Equals"
                type="button"
                (click)="onKey('enter')"
            >
                <tui-icon icon="@tui.equal" />
            </button>
        </tui-keypad>
    </main>
</ng-template>

    
    
      :host {
    display: block;
    block-size: 100%;
}

.calculator {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    padding: 1rem;
    border-radius: 0.5rem;
    /* stylelint-disable-next-line plugin/use-baseline, unit-allowed-list -- dvh: vh spans behind the mobile browser UI and pushes the bottom actions offscreen */
    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);
    }
}