Accessibility pattern · Form inputs

Number input

The field is a text input with inputmode="numeric" and role="spinbutton": phones show a number pad, screen readers learn that the arrow keys step it, and nothing changes when the page is scrolled. Minimum, maximum and whole numbers are enforced with messages that name the rule, and reaching a limit is said in a polite status, not by a button that silently stops working.

WCAG criteria
7
Keyboard rules
5
Checked with
axe, keyboard and the inspector

Live demo

Try it

Use it with a mouse, a keyboard or a screen reader. The inspector beside it shows what the browser tells assistive technology as you go: focus, state changes and announcements.

Darjeeling first flush tea

100 g tin · ₹480 each

Up to 6 per order. Use the buttons, or the Up and Down arrow keys.

Subtotal ₹960

The demo works without JavaScript only as far as its HTML does; the inspector needs JavaScript.

Keyboard

Keys it answers to

Every action works without a pointer.

KeyWhat it does
Arrow Up or Arrow DownAdds or takes away one tin. At 1 or 6 the value stays, and a status message says why.
Home or EndSets the smallest or largest quantity allowed.
EnterChecks a typed number: the subtotal updates, or a message says what to change.
TabMoves on from the field. The minus and plus buttons are skipped, because the arrow keys do their job, unless the option above puts them in the Tab order.
Enter or SpaceOn the minus or plus button, changes the quantity by one; the new quantity is announced.

Screen readers

What it announces

Written from the roles, names and states in the markup.

WhenExpected announcement
Tab reaches the fieldQuantity (tins), spin button, 2. Up to 6 per order. Use the buttons, or the Up and Down arrow keys.
Arrow Up3
Arrow Up reaches 66. Maximum reached: 6 tins per order.
A tap on Increase quantityIncrease quantity, button. 4 tins
Enter on a typed 2.5Enter a whole number of tins.

These are expected announcements, not recordings. Wording and order differ between screen readers and browsers.

Code

Copy the code

The exact files this demo runs on. The styles are served with a prefix that keeps this site's own styles out of the demo; what you copy is the original.

The markup

<div class="ap-number-input" data-ap-number-input data-price="480" data-region="IN">
  <div class="ap-number-input__item">
    <span class="ap-number-input__thumb" aria-hidden="true"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M7 6h10v13.5a1.5 1.5 0 0 1-1.5 1.5h-7A1.5 1.5 0 0 1 7 19.5Z"/><path d="M6 3.5h12V6H6Z"/><path d="M10 11.5c1.2-1.6 2.8-1.6 4 0-1.2 1.6-2.8 1.6-4 0Z"/><path d="M12 13.5V16"/></svg></span>
    <div>
      <h3 class="ap-number-input__name">Darjeeling first flush tea</h3>
      <p class="ap-number-input__meta">100 g tin · ₹480 each</p>
    </div>
  </div>

  <div class="ap-field">
    <label class="ap-label" for="number-input-qty">Quantity (tins)</label>
    <p class="ap-hint" id="number-input-hint">Up to 6 per order. Use the buttons, or the Up and Down arrow keys.</p>
    <div class="ap-number-input__row">
      <div class="ap-number-input__stepper">
        <button type="button" class="ap-number-input__btn" data-ap-step="-1" aria-label="Decrease quantity" aria-controls="number-input-qty" aria-disabled="false" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6 12h12"/></svg></button>
        <input class="ap-number-input__input" id="number-input-qty" type="text" inputmode="numeric" role="spinbutton" aria-valuemin="1" aria-valuemax="6" aria-valuenow="2" value="2" autocomplete="off" aria-describedby="number-input-hint number-input-error" />
        <button type="button" class="ap-number-input__btn" data-ap-step="1" aria-label="Increase quantity" aria-controls="number-input-qty" aria-disabled="false" tabindex="-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 6v12M6 12h12"/></svg></button>
      </div>
      <p class="ap-number-input__total"><span>Subtotal</span> <strong data-ap-total translate="no">₹960</strong></p>
    </div>
    <p class="ap-error ap-number-input__msg" id="number-input-error" aria-live="polite"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 7.5v5.5M12 16.5h.01"/></svg><span data-ap-error></span></p>
    <p class="ap-number-input__note ap-number-input__msg" role="status"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="9"/><path d="M12 11v5.5M12 7.5h.01"/></svg><span data-ap-note></span></p>
  </div>
  <p class="ap-number-input__sr" role="status" data-ap-say></p>
</div>

WCAG 2.2

What it meets

The success criteria this pattern takes care of, and how.

  • 1.3.1 Info and Relationships Level A

    The unit is part of the label, and the limits are in the field's description, so they are heard with the field, not only seen near it.

  • 2.1.1 Keyboard Level A

    The arrow keys, Home and End step the value, and the buttons work with Enter and Space when they are focused.

  • 2.5.8 Target Size (Minimum) Level AA

    The minus and plus buttons are 40 pixels square, far above the 15-pixel spinners of a number field.

  • 3.3.1 Error Identification Level A

    A typed value that breaks a rule is named in text under the field, and the field is marked invalid.

  • 3.3.2 Labels or Instructions Level A

    The label gives the unit and the hint gives the limit before anything is typed.

  • 4.1.2 Name, Role, Value Level A

    role=spinbutton with aria-valuenow, aria-valuemin and aria-valuemax exposes the value and its range; the buttons say what they change.

  • 4.1.3 Status Messages Level AA

    Reaching a limit, and a change made with the buttons, are reported in polite status messages without moving focus.

Usage

When to use it

Use it

  • Small whole numbers that people usually nudge by one or two, like quantities, guests or seats.
  • When the limits are known and worth stating, such as stock or a per-order cap.

Use something else

  • Numbers that are really identifiers, such as phone, card or PIN numbers: use a plain text field.
  • Large values people type in full, like a salary: a text field with the unit is quicker than stepping.
  • A range where the position matters more than the exact value: use a slider.

Common failures

How it usually goes wrong

  • type="number" for everything

    It changes value when someone scrolls the page over it, accepts "e", turns anything it cannot read into an empty value, and its spinner arrows are tiny. A text field with inputmode="numeric" avoids all four.

  • Buttons named "+" and "−"

    A screen reader may read "plus" and "minus", or nothing at all. The buttons are named "Increase quantity" and "Decrease quantity".

  • Silent limits

    A disabled plus button that does nothing leaves people pressing it again. At a limit the button stays pressable, says it is unavailable, and the status explains.

  • The unit only in the design

    "2" next to a picture of a tin means nothing to a screen reader. The unit is in the label: Quantity (tins).

  • Clamping without a word

    Turning a typed 9 into 6 quietly changes someone's order. The value is left as typed and the message says the limit.

Notes

Building it

  • role="spinbutton" is allowed on a text input; it tells screen readers to expect Up and Down, and aria-valuenow carries the number they announce.
  • The buttons have tabindex="-1", as in the WAI-ARIA example, because the arrow keys already do their job. They stay reachable by touch and by a screen reader's reading cursor; the option above shows the other choice.
  • Changes made with the buttons are announced, because focus is on the button, not on the value that changed. Changes made with the arrow keys are not: the spin button itself is heard.
  • inputmode="numeric" asks for a number pad but does not restrict what is typed, so the value is always checked on Enter and when the field is left.
  • The subtotal is formatted with Intl.NumberFormat for India (₹1,920, and ₹1,00,000 for larger sums); it updates quietly, since the quantity is what changed.

Sources: WAI-ARIA Authoring Practices: Spinbutton · GOV.UK Design System: Text input (asking for numbers)

Checked with axe in light and dark themes, at desktop and phone widths, and by keyboard. Not yet tested with every screen reader and browser pair. Report a correction