Skip to content

ExpressiveCSS

Browse documentation

Keypad and Keyboard

On-screen keys that type into a text field: a PIN or dial pad, or a full keyboard.

View as Markdown

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

KeyEffect
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-keyTypes 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-label and point aria-controls at 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 with aria-label.
  • The keypad checks minlength itself, because browsers check it only on what a person types and keypad input comes from a script. A shorter value gets the i18n.tooShort message until it is long enough. The keypad never replaces a message the page set, and a form reset or destroy() clears its own. pattern and required apply 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-size72px
--md-comp-keypad-gap12px
--md-comp-keypad-key-colorsurface-container-highest
--md-comp-keypad-key-label-coloron-surface
--md-comp-keypad-function-key-colorsecondary-container
--md-comp-keypad-function-key-label-coloron-secondary-container
--md-comp-keypad-key-pressed-shape12px; keyboard: 6px
--md-comp-keyboard-columns10
--md-comp-keyboard-gap6px
--md-comp-keyboard-key-height48px
--md-comp-keyboard-number-key-height40px
--md-comp-keyboard-key-shape12px
--md-comp-keyboard-container-colorsurface-container
--md-comp-keyboard-container-shape16px

Options

Name Type Default Description
onKeyFunctionnullCalled 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.tooShortFunctionEnglishTakes 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

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow