Skip to main content

Ask users for Durations

When to use this pattern

This pattern should be used when you need to collect a duration from a user such as asking how long someone has lived at an address.

How to use this pattern

Duration inputs can be created by using the duration component. For example:

<div class="ons-question">
  <fieldset class="ons-fieldset">
    <legend class="ons-fieldset__legend">
      <h1 id="fieldset-legend-title" class="ons-fieldset__legend-title ">How long have you lived at this address?</h1>
      <span class="ons-fieldset__description ons-fieldset__description--title">
        <p>Enter “0” into the years field if you have lived at this address for less than a year</p>
      </span>
    </legend>
    <div class="ons-input-items">
      <div class="ons-field-group">
        <div class="ons-field">
          <span class="ons-input-type">
            <span class="ons-input-type__inner">
              <input type="text" id="address-duration-years" class="ons-input ons-input--text ons-input-type__input ons-input-number--w-2" title="Years" name="address-duration-years" pattern="[0-9]*" inputmode="numeric" />
              <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Years">Years</abbr>
            </span>
          </span>
        </div>
        <div class="ons-field">
          <span class="ons-input-type">
            <span class="ons-input-type__inner">
              <input type="text" id="address-duration-months" class="ons-input ons-input--text ons-input-type__input ons-input-number--w-2" title="Months" name="address-duration-months" pattern="[0-9]*" inputmode="numeric" />
              <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Months">Months</abbr>
            </span>
          </span>
        </div>
      </div>
    </div>
  </fieldset>
</div>
{% from "components/question/_macro.njk" import onsQuestion %}
{% from "components/duration/_macro.njk" import onsDuration %}

{% call onsQuestion({
    "title": "How long have you lived at this address?",
    "description": "<p>Enter “0” into the years field if you have lived at this address for less than a year</p>",
    "legendIsQuestionTitle": true
}) %}
    {{ onsDuration({
        "id": "address-duration",
        "dontWrap": true,
        "field1": {
            "id": "address-duration-years",
            "name": "address-duration-years",
            "suffix": "Years"
        },
        "field2": {
            "id": "address-duration-months",
            "name": "address-duration-months",
            "suffix": "Months"
        }
    }) }}
{% endcall %}
Name Type Required Description
field1 DurationField false Config for the years field
field2 DurationField false Config for the months field
dontWrap boolean false Prevents the input from being wrapped in a field component
legendIsQuestionTitle boolean false Creates a h1 inside the legend further information
mutuallyExclusive MutuallyExclusive (ref) false Configuration object if this is a mutually exclusive input
legendOrLabel string true Text content for the legend. If only a single field is used, a label is created using the value from this property
legendClasses string false Classes for the legend
error Error (ref) false Configuration for validation errors

DurationField

Name Type Required Description
id string true ID for the input
name string true Name attribute for the input
value string false Value for the input
suffix string true Suffix for the input. Used for the title and abbr attributes
attributes object false HTML attributes (for example, data attributes) to add to the element
error boolean false If set to true will style this specific field as if it has an error

{% from "components/mutually-exclusive/_macro.njk" import onsMutuallyExclusive %}
{% from "components/fieldset/_macro.njk" import onsFieldset %}
{% from "components/field/_macro.njk" import onsField %}
{% from "components/input/_macro.njk" import onsInput %}

{% macro onsDuration(params) %}
    {% set numberOfFields = 0 %}

    {% if params.field1 is defined and params.field1 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% if params.field2 is defined and params.field2 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% set fields %}
        {% if params.field1 is defined and params.field1 %}
            
            {{ onsInput({
                "id": params.field1.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field1.error is defined and params.field1.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field1.name,
                "value": params.field1.value,
                "suffix": {
                    "title": params.field1.suffix,
                    "text": params.field1.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field1.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}

        {% if params.field2 is defined and params.field2 %}
            {{ onsInput({
                "id": params.field2.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field2.error is defined and params.field2.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field2.name,
                "value": params.field2.value,
                "suffix": {
                    "title": params.field2.suffix,
                    "text": params.field2.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field2.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}
    {% endset %}

    {% if params.mutuallyExclusive is defined and params.mutuallyExclusive %}
        {% set mutuallyExclusive = params.mutuallyExclusive | setAttributes({
            "id": params.id,
            "legend": params.legend,
            "legendClasses": params.legendClasses,
            "description": params.description,
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}

        {% call onsMutuallyExclusive(mutuallyExclusive) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% elif numberOfFields > 1 %}
        {% call onsFieldset({
            "id": params.id,
            "legend": params.legendOrLabel,
            "description": params.description,
            "legendClasses": 'ons-u-mb-xs ' + (params.legendClasses if params.legendClasses else ''),
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% else %}
        {{ fields | safe }}
    {% endif %}
{% endmacro %}

How to check durations

To help users enter a valid duration, you should:

  • check they have entered something in the duration fields
  • check that what they have entered is valid
  • show an error message if they have not entered anything in one of the fields or what they have entered is not valid

Error messages

Use the correct errors pattern and show the error details above the duration fields.

<div class="ons-question">
  <fieldset class="ons-fieldset">
    <legend class="ons-fieldset__legend">
      <h1 id="fieldset-legend-title" class="ons-fieldset__legend-title ">How long have you lived at this address?</h1>
      <span class="ons-fieldset__description ons-fieldset__description--title">
        <p>Enter “0” into the years field if you have lived at this address for less than a year</p>
      </span>
    </legend>
    <div class="ons-panel ons-panel--error ons-panel--no-title ons-u-mb-s" id="address-duration-error">
      <span class="ons-u-vh">Error: </span>
      <div class="ons-panel__body">
        <p class="ons-panel__error">
          <strong>Enter how long you have lived at this address</strong>
        </p>
        <div class="ons-input-items">
          <div class="ons-field-group">
            <div class="ons-field">
              <span class="ons-input-type">
                <span class="ons-input-type__inner">
                  <input type="text" id="address-duration-years" class="ons-input ons-input--text ons-input-type__input  ons-input--error ons-input-number--w-2" title="Years" name="address-duration-years" pattern="[0-9]*" inputmode="numeric" />
                  <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Years">Years</abbr>
                </span>
              </span>
            </div>
            <div class="ons-field">
              <span class="ons-input-type">
                <span class="ons-input-type__inner">
                  <input type="text" id="address-duration-months" class="ons-input ons-input--text ons-input-type__input  ons-input--error ons-input-number--w-2" title="Months" name="address-duration-months" pattern="[0-9]*" inputmode="numeric" />
                  <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Months">Months</abbr>
                </span>
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </fieldset>
</div>
{% from "components/question/_macro.njk" import onsQuestion %}
{% from "components/duration/_macro.njk" import onsDuration %}

{% call onsQuestion({
    "title": "How long have you lived at this address?",
    "description": "<p>Enter “0” into the years field if you have lived at this address for less than a year</p>",
    "legendIsQuestionTitle": true
}) %}
    {{
        onsDuration({
            "id": "address-duration",
            "dontWrap": true,
            "field1": {
                "id": "address-duration-years",
                "name": "address-duration-years",
                "suffix": "Years"
            },
            "field2": {
                "id": "address-duration-months",
                "name": "address-duration-months",
                "suffix": "Months"
            },
            "error": {
                "id": "address-duration-error",
                "text": "Enter how long you have lived at this address",
                "dsExample": isPatternLib
            }
        })
    }}
{% endcall %}
Name Type Required Description
field1 DurationField false Config for the years field
field2 DurationField false Config for the months field
dontWrap boolean false Prevents the input from being wrapped in a field component
legendIsQuestionTitle boolean false Creates a h1 inside the legend further information
mutuallyExclusive MutuallyExclusive (ref) false Configuration object if this is a mutually exclusive input
legendOrLabel string true Text content for the legend. If only a single field is used, a label is created using the value from this property
legendClasses string false Classes for the legend
error Error (ref) false Configuration for validation errors

DurationField

Name Type Required Description
id string true ID for the input
name string true Name attribute for the input
value string false Value for the input
suffix string true Suffix for the input. Used for the title and abbr attributes
attributes object false HTML attributes (for example, data attributes) to add to the element
error boolean false If set to true will style this specific field as if it has an error

{% from "components/mutually-exclusive/_macro.njk" import onsMutuallyExclusive %}
{% from "components/fieldset/_macro.njk" import onsFieldset %}
{% from "components/field/_macro.njk" import onsField %}
{% from "components/input/_macro.njk" import onsInput %}

{% macro onsDuration(params) %}
    {% set numberOfFields = 0 %}

    {% if params.field1 is defined and params.field1 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% if params.field2 is defined and params.field2 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% set fields %}
        {% if params.field1 is defined and params.field1 %}
            
            {{ onsInput({
                "id": params.field1.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field1.error is defined and params.field1.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field1.name,
                "value": params.field1.value,
                "suffix": {
                    "title": params.field1.suffix,
                    "text": params.field1.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field1.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}

        {% if params.field2 is defined and params.field2 %}
            {{ onsInput({
                "id": params.field2.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field2.error is defined and params.field2.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field2.name,
                "value": params.field2.value,
                "suffix": {
                    "title": params.field2.suffix,
                    "text": params.field2.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field2.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}
    {% endset %}

    {% if params.mutuallyExclusive is defined and params.mutuallyExclusive %}
        {% set mutuallyExclusive = params.mutuallyExclusive | setAttributes({
            "id": params.id,
            "legend": params.legend,
            "legendClasses": params.legendClasses,
            "description": params.description,
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}

        {% call onsMutuallyExclusive(mutuallyExclusive) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% elif numberOfFields > 1 %}
        {% call onsFieldset({
            "id": params.id,
            "legend": params.legendOrLabel,
            "description": params.description,
            "legendClasses": 'ons-u-mb-xs ' + (params.legendClasses if params.legendClasses else ''),
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% else %}
        {{ fields | safe }}
    {% endif %}
{% endmacro %}

If only one of the duration fields is causing a validation error, you can add error styling to just that single field by setting the additional error parameter.

<div class="ons-question">
  <fieldset class="ons-fieldset">
    <legend class="ons-fieldset__legend">
      <h1 id="fieldset-legend-title" class="ons-fieldset__legend-title ">How long do you spend travelling to and from work each day?</h1>
    </legend>
    <div class="ons-panel ons-panel--error ons-panel--no-title ons-u-mb-s" id="duration-error">
      <span class="ons-u-vh">Error: </span>
      <div class="ons-panel__body">
        <p class="ons-panel__error">
          <strong>Enter a number that is less than 60</strong>
        </p>
        <div class="ons-input-items">
          <div class="ons-field-group">
            <div class="ons-field">
              <span class="ons-input-type">
                <span class="ons-input-type__inner">
                  <input type="text" id="address-duration-hours" class="ons-input ons-input--text ons-input-type__input ons-input-number--w-2" title="Hours" name="address-duration-hours" value="2" pattern="[0-9]*" inputmode="numeric" />
                  <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Hours">Hours</abbr>
                </span>
              </span>
            </div>
            <div class="ons-field">
              <span class="ons-input-type">
                <span class="ons-input-type__inner">
                  <input type="text" id="address-duration-minutes" class="ons-input ons-input--text ons-input-type__input  ons-input--error ons-input-number--w-2" title="Minutes" name="address-duration-minutes" value="60" pattern="[0-9]*" inputmode="numeric" />
                  <abbr class="ons-input-type__type ons-js-input-abbr" aria-hidden="true" title="Minutes">Minutes</abbr>
                </span>
              </span>
            </div>
          </div>
        </div>
      </div>
    </div>
  </fieldset>
</div>
{% from "components/question/_macro.njk" import onsQuestion %}
{% from "components/duration/_macro.njk" import onsDuration %}

{% call onsQuestion({
    "title": "How long do you spend travelling to and from work each day?",
    "legendIsQuestionTitle": true
}) %}
    {{ onsDuration({
        "id": "address-duration",
        "dontWrap": true,
        "error": {
            "text": "Enter a number that is less than 60",
            "id": "duration-error"
        },
        "field1": {
            "id": "address-duration-hours",
            "name": "address-duration-hours",
            "suffix": "Hours",
            "value": "2"
        },
        "field2": {
            "id": "address-duration-minutes",
            "name": "address-duration-minutes",
            "suffix": "Minutes",
            "value": "60",
            "error": true
        }
    }) }}
{% endcall %}
Name Type Required Description
field1 DurationField false Config for the years field
field2 DurationField false Config for the months field
dontWrap boolean false Prevents the input from being wrapped in a field component
legendIsQuestionTitle boolean false Creates a h1 inside the legend further information
mutuallyExclusive MutuallyExclusive (ref) false Configuration object if this is a mutually exclusive input
legendOrLabel string true Text content for the legend. If only a single field is used, a label is created using the value from this property
legendClasses string false Classes for the legend
error Error (ref) false Configuration for validation errors

DurationField

Name Type Required Description
id string true ID for the input
name string true Name attribute for the input
value string false Value for the input
suffix string true Suffix for the input. Used for the title and abbr attributes
attributes object false HTML attributes (for example, data attributes) to add to the element
error boolean false If set to true will style this specific field as if it has an error

{% from "components/mutually-exclusive/_macro.njk" import onsMutuallyExclusive %}
{% from "components/fieldset/_macro.njk" import onsFieldset %}
{% from "components/field/_macro.njk" import onsField %}
{% from "components/input/_macro.njk" import onsInput %}

{% macro onsDuration(params) %}
    {% set numberOfFields = 0 %}

    {% if params.field1 is defined and params.field1 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% if params.field2 is defined and params.field2 %}
        {% set numberOfFields = numberOfFields + 1 %}
    {% endif %}

    {% set fields %}
        {% if params.field1 is defined and params.field1 %}
            
            {{ onsInput({
                "id": params.field1.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field1.error is defined and params.field1.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field1.name,
                "value": params.field1.value,
                "suffix": {
                    "title": params.field1.suffix,
                    "text": params.field1.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field1.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}

        {% if params.field2 is defined and params.field2 %}
            {{ onsInput({
                "id": params.field2.id,
                "classes": (" ons-input--error" if (params.error is defined and params.error and params.field2.error is defined and params.field2.error) or (numberOfFields > 1 and params.error is defined and params.error and not params.field1.error and not params.field2.error) else "") + (" ons-js-exclusive-group-item" if params.mutuallyExclusive else ""),
                "width": "2",
                "type": "number",
                "name": params.field2.name,
                "value": params.field2.value,
                "suffix": {
                    "title": params.field2.suffix,
                    "text": params.field2.suffix
                },
                "label": {
                    "text": params.legendOrLabel if numberOfFields < 2,
                    "description": params.description if numberOfFields < 2
                },
                "attributes": params.field2.attributes,
                "fieldId": params.id if numberOfFields < 2,
                "error": params.error if numberOfFields < 2
            }) }}
        {% endif %}
    {% endset %}

    {% if params.mutuallyExclusive is defined and params.mutuallyExclusive %}
        {% set mutuallyExclusive = params.mutuallyExclusive | setAttributes({
            "id": params.id,
            "legend": params.legend,
            "legendClasses": params.legendClasses,
            "description": params.description,
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}

        {% call onsMutuallyExclusive(mutuallyExclusive) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% elif numberOfFields > 1 %}
        {% call onsFieldset({
            "id": params.id,
            "legend": params.legendOrLabel,
            "description": params.description,
            "legendClasses": 'ons-u-mb-xs ' + (params.legendClasses if params.legendClasses else ''),
            "error": params.error,
            "legendIsQuestionTitle": params.legendIsQuestionTitle,
            "dontWrap": params.dontWrap
        }) %}
            <div class="ons-field-group">
                {{ fields | safe }}
            </div>
        {% endcall %}
    {% else %}
        {{ fields | safe }}
    {% endif %}
{% endmacro %}

If all fields are empty

Use “Enter [whatever the duration is]”. For example, “Enter how long you have lived at this address”.

If one of the fields is empty

Use “Enter the number of [whatever the duration is]”.
For example, “Enter the number of years”.

If what is entered in one of the fields is not a number

Use “Enter a number for [invalid field], for example, [appropriate number]”.
For example, “Enter a number for months, for example, 7”.

If what is entered in a field is above the maximum value for that field

Use “Enter a number that is less than [whatever the maximum is for the duration]”.
For example, “Enter a number that is less than 12” for months.

Help improve this pattern

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