Skip to main content

Ask users for Phone numbers

How to ask users for a phone number.

<div class="ons-field">
  <label class="ons-label  ons-label--with-description " for="telephone">UK mobile number
  </label>
  <span id="description-hint" class="ons-label__description  ons-input--with-description">
    This will not be stored and only used once to send your access code
  </span>
  <input type="tel" id="telephone" class="ons-input ons-input--text ons-input-type__input ons-input-number--w-15" autocomplete="tel" aria-describedby="description-hint" />
</div>
{% from "components/input/_macro.njk" import onsInput %}
{{
    onsInput({
        "id": "telephone",
        "type": "tel",
        "autocomplete": "tel",
        "width": "15",
        "label": {
            "text": "UK mobile number",
            "description": "This will not be stored and only used once to send your access code"
        }
    })
}}
Name Type Required Description
id string true The id of the input. This will also be added to the label if a label is specified
type string false The type of the input, for example, number, email, tel. Will default to text
classes string false Classes to add to the input.
width string false Required width of the input. Will also accept the bp-suffix e.g. '7@m'
name string false The name of the input
value string | number false The value to set the input to
min number false Minimum accepted number or date
max number false Maximum accepted number or date
minLength number false Minimum accepted length of input value
maxLength number false Maximum accepted length of input value
attributes object false HTML attributes (for example, data attributes) to add to the input
label Label (ref) false Settings for the input label. for will automatically be set to match the input id
prefix InputPrefix false Settings to prefix the input with
suffix InputSuffix false Settings to suffix the input with
fieldId string false Id for the field
fieldClasses string false Classes for the field
dontWrap boolean false Prevents the input from being wrapped in a field component
mutuallyExclusive MutuallyExclusive (ref) false Configuration object if this is a mutually exclusive input
CharCheckLimit CharCheckLimit false Configuration object if this input has a character count
legend string Only if mutuallyExclusive is set Text content for the legend
legendClasses string false Classes for the legend
error Error (ref) false Configuration for validation errors
autocomplete string true Autocomplete attribute used to override the browsers native autocomplete
accessiblePlaceholder boolean false Will add the provided label as an accessible placeholder
searchButton Button (ref) false Settings for the button used for a search pattern.
postTextboxLinkText string false The text for the link to follow the textbox
postTextboxLinkUrl string false The url for the link to follow the textbox
listeners object false Creates a script element that adds an event listener to the element by id. Takes key { event } and value { function }
required boolean false Adds the required attribute to the input to indicate that the user must specify a value for the input before the owning form can be submitted

Prefix/Suffix

Name Type Required Description
text string true The text for the prefix/suffix
title string true The title of the prefix/suffix. For example where text is “cm”, title would be “centimetres”
id string false Id for the prefix/suffix

CharCheckLimit

Name Type Required Description
limit number false The maximum amount of characters a user should type in
charcheckCountdown boolean false Displays the number of remaining characters allowed based on the limit
charCountPlural string false Required if CharCheckLimit is supplied. The string that will render how many characters are remaining. {x} Will be replaced with the number
charCountSingular string false Required if CharCheckLimit is supplied. The string that will render how many characters are remaining (singular). {x} Will be replaced with the number
charCountOverLimitSingular string false Required if CharCheckLimit is supplied. The string that will render how many characters are over (singular). {x} Will be replaced with the number
charCountOverLimitPlural string false Required if CharCheckLimit is supplied. The string that will render how many characters are over (plural). {x} Will be replaced with the number
{% macro onsInput(params) %}
    {% from "components/mutually-exclusive/_macro.njk" import onsMutuallyExclusive %}
    {% from "components/char-check-limit/_macro.njk" import onsCharLimit %}
    {% from "components/search/_macro.njk" import onsSearch %}
    {% from "components/field/_macro.njk" import onsField %}
    {% from "components/label/_macro.njk" import onsLabel %}

    {% if params.type == "number" %}
        {# Type must be "text" or Firefox and Safari will set a blank value to the server if non numeric characters are entered -
        they don’t block non numeric characters: https://bugzilla.mozilla.org/show_bug.cgi?id=1398528 #}
        {% set type = "text" %}
        {% set pattern = "[0-9]*" %}
        {% set inputmode = "numeric" %}
    {% elif params.type is defined and params.type %}
        {% set type = params.type %}
    {% elif params.searchButton is defined and params.searchButton %}
        {% set type = "search" %}
    {% else %}
        {% set type = "text" %}
    {% endif %}

    {% set exclusiveClass = " ons-js-exclusive-group-item" if params.mutuallyExclusive else "" %}
    {% set inputPlaceholder = ' ons-input--placeholder' if params.accessiblePlaceholder else "" %}

    {% set input %}
        <input
            type="{{ type }}"
            id="{{ params.id }}"
            class="ons-input ons-input--text ons-input-type__input{% if params.error is defined and params.error %} ons-input--error{% endif %}{% if params.searchButton is defined and params.searchButton %} ons-search__input{% endif %}{% if params.classes is defined and params.classes %} {{ params.classes }}{% endif %}{% if params.width is defined and params.width %} ons-input{% if params.type is defined and (params.type == 'number' or params.type == 'tel') %}-number{% endif %}--w-{{ params.width }}{% endif %}{{ exclusiveClass }}{{ inputPlaceholder }}"
            {% if params.prefix is defined and params.prefix or params.suffix is defined and params.suffix %}title="{{ params.prefix.title if params.prefix }}{{ params.suffix.title if params.suffix }}"{% endif %}
            {% if params.name is defined and params.name %}name="{{ params.name }}"{% endif %}
            {% if params.value is defined and params.value %}value="{{ params.value }}"{% endif %}
            {% if params.accept is defined and params.accept %}accept="{{ params.accept }}"{% endif %}
            {% if params.min is defined and params.min %}min="{{ params.min }}"{% endif %}
            {% if params.max is defined and params.max %}max="{{ params.max }}"{% endif %}
            {% if params.minLength is defined and params.minLength %}minlength="{{ params.minLength }}"{% endif %}
            {% if params.maxLength is defined and params.maxLength %}maxlength="{{ params.maxLength }}"{% endif %}
            {% if pattern is defined and pattern %}pattern="{{ pattern }}"{% endif %}
            {% if inputmode is defined and inputmode %}inputmode="{{ inputmode }}"{% endif %}
            {% if params.autocomplete is defined and params.autocomplete %}autocomplete="{{ params.autocomplete }}"{% endif %}
            {% if params.accessiblePlaceholder is defined and params.accessiblePlaceholder %}placeholder="{{ params.label.text }}"{% endif %}
            {% if params.charCheckLimit is defined and params.charCheckLimit %}data-char-check-ref="{{ params.id }}-check-remaining" data-char-check-num="{{ params.charCheckLimit.limit }}" aria-describedby="{{ params.id }}-check-remaining"{% endif %}
            {% if params.charCheckLimit is defined and params.charCheckLimit and params.charCheckLimit.charcheckCountdown is defined and params.charCheckLimit.charcheckCountdown %}data-char-check-countdown="true"{% endif %}
            {% if params.attributes is defined and params.attributes %}{% for attribute, value in (params.attributes.items() if params.attributes is mapping and params.attributes.items else params.attributes) %}{{ attribute }}{% if value is defined and value %}="{{ value }}"{% endif %} {% endfor %}{% endif %}
            {% if params.label is defined and params.label and params.label.description is defined and params.label.description %}{% if params.label.id is defined and params.label.id %}aria-describedby="{{ params.label.id }}-description-hint"{% else %}aria-describedby="description-hint"{% endif %}{% endif %}
            {% if params.required is defined and params.required == true %}required="required"{% endif %}
        />
        {% if params.listeners %}
            <script{% if csp_nonce %} nonce="{{ csp_nonce() }}"{% endif %}>
            {% for listener, value in (params.listeners.items() if params.listeners is mapping and params.listeners.items else params.listeners) %}
                document.getElementById("{{ params.id }}").addEventListener('{{ listener }}', function(){ {{ value }} });
            {% endfor %}
            </script>
        {% endif %}
        {% if params.postTextboxLinkText is defined and params.postTextboxLinkText %}
            <a class="ons-u-fs-s" href="{{ params.postTextboxLinkUrl }}">{{ params.postTextboxLinkText }}</a>
        {% endif %}
    {% endset %}
    {% set field %}
        {% if params.label is defined and params.label and params.label.text is defined and params.label.text %}
            {{ onsLabel({
                "for": params.id,
                "id": params.label.id,
                "text": params.label.text,
                "classes": params.label.classes,
                "description": params.label.description,
                "attributes": params.label.attributes,
                "accessiblePlaceholder": params.accessiblePlaceholder
            }) }}
        {% endif %}

        {% if params.prefix is defined and params.prefix or params.suffix is defined and params.suffix %}
            {% if params.prefix is defined and params.prefix %}
                {% set prefixClass = " ons-input-type--prefix" %}
            {% endif %}

            <span class="ons-input-type{{ prefixClass }}">
                <span class="ons-input-type__inner">
                    {{ input | safe }}
                    {% set abbr = params.prefix or params.suffix %}
                    <abbr
                        class="ons-input-type__type ons-js-input-abbr"
                        aria-hidden="true"
                        title="{{ abbr.title }}"
                        {% if abbr.id is defined and abbr.id %} id="{{ abbr.id }}"{% endif %}
                        >{{ abbr.text or abbr.title }}</abbr>
                </span>
            </span>
        {% elif params.searchButton is defined and params.searchButton %}
            <span class="ons-grid--flex ons-search">
                {% call onsSearch({
                    "accessiblePlaceholder": params.accessiblePlaceholder,
                    "searchButton": {
                        "type": params.searchButton.type,
                        "text": params.searchButton.text,
                        "id": params.searchButton.id,
                        "attributes": params.searchButton.attributes,
                        "classes": params.searchButton.classes,
                        "iconType": params.searchButton.iconType
                    }
                }) %}
                    {{ input | safe }}
                {% endcall %}
            </span>
        {% else %}
            {{ input | safe }}
        {% endif %}
    {% endset %}

    {% if params.charCheckLimit is defined and params.charCheckLimit and params.charCheckLimit.limit is defined and params.charCheckLimit.limit %}
        {% set charCheckField %}
            {% call onsCharLimit({
                "id": params.id ~ "-check",
                "limit": params.charCheckLimit.limit,
                "type": "check",
                "charCountSingular": params.charCheckLimit.charCountSingular,
                "charCountPlural": params.charCheckLimit.charCountPlural,
                "charCountOverLimitSingular": params.charCheckLimit.charCountOverLimitSingular,
                "charCountOverLimitPlural": params.charCheckLimit.charCountOverLimitPlural
            }) %}
                {{ field | safe }}
            {% endcall %}
        {% endset %}
    {% endif %}

    {% if params.mutuallyExclusive is defined and params.mutuallyExclusive %}
        {% call onsMutuallyExclusive({
            "id": params.fieldId,
            "legend": params.legend,
            "legendClasses": params.legendClasses ~ "ons-js-input-legend",
            "description": params.description,
            "dontWrap": params.dontWrap,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "checkbox": params.mutuallyExclusive.checkbox,
            "or": params.mutuallyExclusive.or,
            "deselectMessage": params.mutuallyExclusive.deselectMessage,
            "deselectGroupAdjective": params.mutuallyExclusive.deselectGroupAdjective,
            "deselectCheckboxAdjective": params.mutuallyExclusive.deselectCheckboxAdjective,
            "error": params.error,
            "autosuggestResults": params.autosuggestResults
        }) %}
            {% if charCheckField is defined and charCheckField %}
                {{ charCheckField | safe }}
            {% else %}
                {{ field | safe }}
            {% endif %}
        {% endcall %}
    {% elif type == "hidden" %}
        {{ field | safe }}
    {% else %}
        {% call onsField({
            "id": params.fieldId,
            "classes": params.fieldClasses,
            "dontWrap": params.dontWrap,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "error": params.error
        }) %}
            {% if charCheckField is defined and charCheckField %}
                {{ charCheckField | safe }}
            {% else %}
                {{ field | safe }}
            {% endif %}
        {% endcall %}
    {% endif %}
{% endmacro %}
.ons-input-type {
  display: block;

  // Keep the entire component display block, but use inline-flex on inner to prevent the orange focus from going full width
  &__inner {
    display: inline-flex;
    position: relative;
  }

  // Double ampersand is needed to solve specificity issues
  & &__input {
    flex: 1 1 auto;
    position: relative;
    z-index: 1;

    &:focus {
      box-shadow: inset 0 0 0 1px $color-input;
    }
  }

  &__type[title] {
    background-color: $color-button-secondary;
    border: 1px solid $color-input;
    display: block;
    flex: 0 0 auto;
    font-size: 1rem;
    font-weight: $font-weight-bold;
    line-height: normal;
    padding: $input-padding-vertical $input-padding-horizontal * 2;
    text-align: center;
    text-decoration: none;
    white-space: nowrap;
  }

  &__input:focus + &__type[title]::after {
    border-radius: $input-radius;
    bottom: 0;
    box-shadow: 0 0 0 3px $color-focus;
    content: '';
    display: block;
    left: 0;
    position: absolute;
    right: 0;
    top: 0;
  }

  &:not(&--prefix) & {
    &__type[title] {
      border-left: 0;
      border-radius: 0 $input-radius $input-radius 0;
    }

    &__input {
      border-radius: $input-radius 0 0 $input-radius;
    }
  }

  &--prefix & {
    &__type[title] {
      border-radius: $input-radius 0 0 $input-radius;
      border-right: 0;
      order: 0;
    }

    &__input {
      border-radius: 0 $input-radius $input-radius 0;
      order: 1;
    }
  }
}

.ons-input {
  border: $input-border-width solid $color-input;
  border-radius: $input-radius;
  color: inherit;
  display: block;
  font-family: inherit;
  font-size: 1rem;
  line-height: 1rem;
  padding: $input-padding-vertical $input-padding-horizontal;
  position: relative;
  width: 100%;
  z-index: 3;

  &::-ms-clear {
    display: none;
  }

  @include mq(s) {
    &--text,
    &--select {
      &:not(.ons-input--block):not([class*='input--w-']) {
        width: $input-width;
      }
    }
  }

  &--text,
  &--textarea {
    // Prevent inner shadow on iOS
    appearance: none;
  }

  &:focus {
    box-shadow: 0 0 0 3px $color-focus, inset 0 0 0 1px $color-input;
    outline: none;
  }

  &:disabled {
    border-color: $color-grey-75;
    cursor: not-allowed;
  }

  &--error:not(:focus) {
    border: 1px solid $color-ruby-red;
    box-shadow: inset 0 0 0 1px $color-ruby-red;
  }

  &--with-description {
    margin-bottom: 0.55rem;
  }
}

// Text input widths
@include input-width('ons-input--w-{x}');

// Number input widths
@include input-width('ons-input-number--w-{x}', 0.54rem);

.ons-input--postcode {
  max-width: input-width-calc($chars: 5, $num-chars: 2, $spaces: 1);
  width: 100%;
}

.ons-input__helper {
  font-size: 0.8rem;
  font-weight: $font-weight-bold;
  margin-top: 0.2rem;
}

.ons-input--select {
  appearance: none;
  background: $color-white url('#{$static}/img/icons--chevron-down.svg') no-repeat center right 10px;
  background-size: 1rem;
  line-height: 1.3rem;
  padding: 0.39rem 2rem 0.39rem $input-padding-horizontal;

  &::-ms-expand {
    display: none;
  }
}

.ons-input--textarea {
  line-height: normal;
  resize: vertical;
  width: 100%;
}

.ons-input--block {
  display: block;
  width: 100%;
}

.ons-input--placeholder {
  background: transparent;
  &::placeholder {
    color: transparent;
  }
  &:valid:not(:placeholder-shown) {
    background-color: $color-white;
  }
  &:focus {
    background-color: $color-white;
  }
}

.ons-input--limit-reached:not(:focus) {
  border: $input-border-width solid $color-ruby-red;
}

.ons-input__limit {
  display: block;

  &--reached {
    color: $color-ruby-red;
  }
}

.ons-input--ghost {
  border: 2px solid rgba(255, 255, 255, 0.6);
  &:focus {
    border: 2px solid $color-input;
  }
}

.ons-input-search {
  @extend .ons-input--block;

  &--icon {
    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23ffffff'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E");
    background-position: 12px 10px;
    background-repeat: no-repeat;
    background-size: 18px 18px;
    padding-left: 2.4rem;

    &:focus,
    &:active,
    &:valid:not(:placeholder-shown) {
      background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' fill='%23000000'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M11.86 10.23 8.62 6.99a4.63 4.63 0 1 0-6.34 1.64 4.55 4.55 0 0 0 2.36.64 4.65 4.65 0 0 0 2.33-.65l3.24 3.23a.46.46 0 0 0 .65 0l1-1a.48.48 0 0 0 0-.62Zm-5-3.32a3.28 3.28 0 0 1-2.31.93 3.22 3.22 0 1 1 2.35-.93Z'/%3E%3C/svg%3E");
    }
    &:focus,
    &:active {
      background-position: 12px 10px;
      box-shadow: 0 0 0 3px $color-focus;
    }
  }
}

When to use this pattern

Only ask users for a phone number when your service has a genuine need for this information. For example, to send an access code by text message.

How to use this pattern

Use an input component and set the input type to tel to bring up the correct keypad on touch devices.

When using this pattern you should:

  • make it clear what type of telephone number you need
  • tell users why you need a phone number and what it will be used for
  • let users know if you will contact them and when
  • make sure you allow the user to enter a phone number in a familiar format
  • help users enter a phone number in the correct format

Make it clear what type of phone number you need

Use the input label with the input component to let users know what type of phone number you need, for example, a UK, international or mobile phone number.

Explain why you need the phone number

Use the optional ons-label__description for hint text to reassure users why you need a phone number and what it will be used for.

Allow all phone number formats

The phone number field must accommodate all phone number variations so the user can enter a number in the format they are used to. The length of the field can be adjusted using the input width classes to allow for any spaces or common characters, for example, brackets, dashes or country codes.

Validate phone numbers

To help users enter a valid phone number, you should:

  • check they have entered the phone number correctly
  • show error validation messages if they have not entered a valid phone number
  • allow them to paste the phone number
  • ask them to confirm it is correct using the check answers pattern

Error messages

Use the correct errors pattern and show the error details above the phone number field if it is missing or incorrect.

<div class="ons-panel ons-panel--error ons-panel--no-title ons-u-mb-s" id="email-error">
  <span class="ons-u-vh">Error: </span>
  <div class="ons-panel__body">
    <p class="ons-panel__error">
      <strong>Enter a UK mobile number in a valid format, for example, 07700 912345 or +44 7700 912345</strong>
    </p>
    <div class="ons-field">
      <label class="ons-label  ons-label--with-description " for="tel">UK mobile number
      </label>
      <span id="description-hint" class="ons-label__description  ons-input--with-description">
        This will not be stored and only used once to send the access code
      </span>
      <input type="tel" id="tel" class="ons-input ons-input--text ons-input-type__input ons-input--error ons-input-number--w-15" value="not a number" autocomplete="telephone" required="true" aria-describedby="description-hint" />
    </div>
  </div>
</div>
{% from "components/input/_macro.njk" import onsInput %}
{{
    onsInput({
        "id": "tel",
        "width": "15",
        "type": "tel",
        "autocomplete": "telephone",
        "value": "not a number",
        "label": {
            "text": "UK mobile number",
            "description": "This will not be stored and only used once to send the access code"
        },
        "attributes": {
            "required": true
        },
        "error": {
            "id": "email-error",
            "text": "Enter a UK mobile number in a valid format, for example, 07700 912345 or +44 7700 912345",
            "dsExample": isPatternLib
        }
    })
}}

If the phone number is missing

Use “Enter [the required type of phone number]”, for example, “Enter a UK mobile number”.

If a phone number is incorrect

Use “Enter a UK mobile number in a valid format, for example, 07700 912345 or +44 7700 912345”, to give them a clear example of a valid phone number.

Make sure any examples of phone numbers in the error message are suitable for the type of number you are asking for. Ofcom maintains a list of numbers  reserved for use in media that you can use.

Use autocomplete

Use the autocomplete attribute when you are asking for a phone number as it helps the user fill out a form more quickly.

To do this, set the autocomplete attribute on the input field to tel.

You will need to use the autocomplete attribute to meet WCAG 2.1 AA standards for accessibility 

Help improve this pattern

Let us know how we could improve this pattern or share your user research findings. Discuss the ‘Phone numbers’ pattern on GitHub