A keypad or keyboard is a set of on-screen keys that type into a text field.
A .keypad is three columns of round keys for PINs,
codes and amounts. A .keyboard is rows of keys for
text. Give either one role="group" and a name, and
point aria-controls at the field it types into. Make
every key a <button type="button">. A key with a
value types it at the caret, and the field gets the
same input event typing would fire.
AutoInit() starts every .keypad and
.keyboard.
<div class="field outlined">
<input id="pin" type="password" inputmode="none" maxlength="6" placeholder=" " autocomplete="off">
<label for="pin">PIN</label>
</div>
<div class="keypad" role="group" aria-label="PIN keypad" aria-controls="pin">
<button type="button" value="1">1</button>
<button type="button" value="2">2</button>
<!-- … 3 to 9 … -->
<span></span>
<button type="button" value="0">0</button>
<button type="button" data-key="Backspace" aria-label="Delete">
<span class="material-symbols" aria-hidden="true">backspace</span>
</button>
</div>
Dial pad
Put a key's letters in a <small> after the digit.
A key with a button style class keeps that style, so a call key is
a filled button. Add wide to a key to span
two cells as a pill. That fills the last row beside Backspace.
<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
<button type="button" value="2">2<small aria-hidden="true">ABC</small></button>
<!-- … -->
<button type="button" class="filled wide" data-key="Enter" aria-label="Call">
<span class="material-symbols" aria-hidden="true">call</span>
</button>
<button type="button" data-key="Backspace" aria-label="Delete">
<span class="material-symbols" aria-hidden="true">backspace</span>
</button>
</div>
Backspace in the field
For a centered call key, move Backspace into the text field as a
trailing icon-button suffix. Hold the cells beside
the call key open with empty <span> elements. A
data-key key inside the field's .field works
like a keypad key, even when the field renders after the keypad.
Other buttons there, such as a password toggle, keep their own behavior.
<div class="field">
<input id="phone" type="tel" inputmode="none" placeholder=" " autocomplete="off">
<label for="phone">Phone number</label>
<button type="button" class="icon-button suffix" data-key="Backspace" aria-label="Delete">
<span class="material-symbols" aria-hidden="true">backspace</span>
</button>
</div>
<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
<!-- 1 to # -->
<span></span>
<button type="button" class="filled" data-key="Enter" aria-label="Call">
<span class="material-symbols" aria-hidden="true">call</span>
</button>
<span></span>
</div>
A third action
To put a page action such as contacts in the empty cell, give the
key a data-key the script does not know. That key types
nothing and calls onKey with its name.
<div class="keypad" role="group" aria-label="Dial pad" aria-controls="phone">
<!-- 1 to # -->
<button type="button" data-key="Contacts" aria-label="Contacts">
<span class="material-symbols" aria-hidden="true">contacts</span>
</button>
<button type="button" class="filled" data-key="Enter" aria-label="Call">
<span class="material-symbols" aria-hidden="true">call</span>
</button>
<button type="button" data-key="Backspace" aria-label="Delete">
<span class="material-symbols" aria-hidden="true">backspace</span>
</button>
</div>
<script>
Expressive.Keypad.init(document.querySelector('.keypad'), {
onKey: (key) => { if (key === 'Contacts') openContacts(); },
});
</script>
Keyboard
Each child of a .keyboard is a row. A character key is
a tenth of the row and may shrink. A function key is one and a half
keys wide, and the space bar takes what is left. A nine-key row is
centered, which gives QWERTY its half-key indent. Set
--md-comp-keyboard-columns for a layout with more or
fewer keys in its widest row.
An optional <div class="numbers"> row of digits
sits above the letters, with shorter 40dp keys. To drop it, leave it
out or add hidden. Remove hidden to show
it again.
<div class="keyboard" role="group" aria-label="Keyboard" aria-controls="message">
<div class="numbers">
<button type="button" value="1">1</button>
<!-- … 2 to 0 … -->
</div>
<div>
<button type="button" value="q">q</button>
<!-- … w to p … -->
</div>
<div><!-- a to l --></div>
<div>
<button type="button" data-key="Shift" aria-pressed="false" aria-label="Shift">
<span class="material-symbols" aria-hidden="true">shift</span>
</button>
<!-- z to m -->
<button type="button" data-key="Backspace" aria-label="Delete">
<span class="material-symbols" aria-hidden="true">backspace</span>
</button>
</div>
<div>
<button type="button" value=",">,</button>
<button type="button" value=" " aria-label="Space"></button>
<button type="button" value=".">.</button>
<button type="button" data-key="Enter" aria-label="New line">
<span class="material-symbols" aria-hidden="true">keyboard_return</span>
</button>
</div>
</div>
Keys
| Key | Effect |
|---|---|
value="…" | Types the value at the caret, replacing any selection. Typing stops at the field's maxlength. |
data-key="Backspace" | Deletes the selection, or the character before the caret. |
data-key="Enter" | Submits the field's form with requestSubmit(), so validation runs. In a <textarea> it types a new line. |
data-key="Shift" | Toggles aria-pressed. While pressed the character keys show capitals, and the next one types a capital and releases Shift. |
data-key="CapsLock" | Toggles aria-pressed. While pressed the character keys show and type capitals, until it is pressed again. |
data-key="Layer" | Shows the elements in the keyboard whose data-layer matches the key's value, such as a row of symbols in place of the letters. Hides those with another data-layer. Elements without data-layer always show. |
Any other data-key | Types nothing and calls onKey with its name. |
The script ignores a <button> without
value or data-key, because it is not a key.
Keys type at the caret. In a field nobody has clicked into, they
type at the end. While the field has focus, holding down a hardware
key presses the matching on-screen key, so the keyboard shows what
is typed.
Number and email inputs have no caret position to read, so keys
type at the end. A number input empties itself on a partial value
such as 12. or -, so the script ignores
that key there. For decimals and signs, use a text field with
inputmode="none".
Accessibility
- Name the group with
aria-labeland pointaria-controlsat the field. Give the field its own label. - Give every key
type="button", or a key inside a form submits it. Name icon keys and the space bar witharia-label. - The keypad checks
minlengthitself, because browsers check it only on what a person types and keypad input comes from a script. A shorter value gets thei18n.tooShortmessage until it is long enough. The keypad never replaces a message the page set, and a form reset ordestroy()clears its own.patternandrequiredapply as usual. - Add
inputmode="none"to the field so a phone or tablet keeps its own keyboard closed. A hardware keyboard still types into the field. - A pointer press keeps focus and the caret in the field while it has focus. Keys stay native buttons, so Tab, Enter and Space reach them.
- Keys are 72dp on a keypad and 48dp tall on a keyboard. Keep a keyboard wide enough that a character key stays at least 24px wide.
Tokens
| Token | Default |
|---|---|
--md-comp-keypad-key-size | 72px |
--md-comp-keypad-gap | 12px |
--md-comp-keypad-key-color | surface-container-highest |
--md-comp-keypad-key-label-color | on-surface |
--md-comp-keypad-function-key-color | secondary-container |
--md-comp-keypad-function-key-label-color | on-secondary-container |
--md-comp-keypad-key-pressed-shape | 12px; keyboard: 6px |
--md-comp-keyboard-columns | 10 |
--md-comp-keyboard-gap | 6px |
--md-comp-keyboard-key-height | 48px |
--md-comp-keyboard-number-key-height | 40px |
--md-comp-keyboard-key-shape | 12px |
--md-comp-keyboard-container-color | surface-container |
--md-comp-keyboard-container-shape | 16px |
Options
| Name | Type | Default | Description |
|---|---|---|---|
onKey | Function | null | Called after every key press with the text typed ('7', 'A', ' ') or the function key's name ('Backspace', 'Enter', 'Shift', 'CapsLock', 'Layer'). Works without aria-controls. |
i18n.tooShort | Function | English | Takes the field's minlength and returns the validation message for a shorter keypad-typed value: Use at least 3 characters. |
Methods
const keypad = Expressive.Keypad.getInstance(el);
keypad.target; // the field named by aria-controls, or null
keypad.destroy(); // removes the listeners