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> ).
(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;
}
<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%;
}
—
<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;
}
<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;
}
}
<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);
}
}