This guide will be deprecated soon!

CommCare HQ is currently undergoing a migration to Bootstrap 5. You can find the updated style guide here.

As we connect atoms to build molecules, it’s important to keep some principles in mind.

First, don’t try to do too much at once; keep concepts and design focused. Hick’s Law predicts the time it takes to make a decision increases with the number and complexity of choices available: the more stimuli, the more time it takes to respond to the stimuli. If you’d like to read more, there is plenty of research on the phenomenon of analysis paralysis.

Recommended reading: Laws of UX

Customers have expressed frustration when figuring out where "the right" button is in CommCare: there are too many options.

Most buttons shoud use the "call to action" indigo (see guidance on colors). This color is used only in this fashion on HQ, which establishes a pattern and makes buttons easier to identify.

Primary buttons are solid indigo (.btn-primary), while secondary/tertiary buttons use an outline style (.btn-default). Most pages have a single primary action - often save, download, or add - and may have multiple secondary or tertiary buttons. The names are to suggest what level of importance the button has to the user. If you have a row of buttons (4+), please reconsider the design.

If a primary button is on a dark background or a vibrant blue, the "call to action" indigo looks washed out. In these areas, use cyan (.btn-info). This is done in form builder. The login/signup workflows also use slightly different button colors, because those pages have a color scheme more like that of dimagi.com than of HQ. If working in one of these areas, match the existing color palette and button conventions.

We use a subset of the buttons provided by Bootstrap and restyle some of them. We avoid .btn-success and .btn-warning, but we do use .btn-danger for destructive actions like deleting or archiving objects. We use .btn-info occasionally, either for primary actions on dark backgrounds as described above or for secondary actions that are a download.

Bootstrap documentation

Summary

Most buttons
<button class='btn btn-default'>
          Default
</button>
The most important action on a page
<button class='btn btn-primary'>
          Primary
</button>
Destructive actions like deletion or archiving
<button class='btn btn-danger'>
          <i class='fa fa-remove'></i>
          Danger
</button>
Secondary doownload action
Primary action for dark backgrounds
<button class='btn btn-info'>
          <i class='fa fa-cloud-download'></i>
          Info
</button>

HQ has many different interactions for selecting data from a list. The best of these are toggles (for short lists) and select2 (for long lists).

For any user-defined data, it's difficult to be certain how many items will be in the list. Even data sets that we might expect to be small - such as the number of forms in a module - might be large for certain projects. It's better to assume that a list will grow large and display it as some kind of dropdown where the options are a click away, rather than to assume it'll stay small and display all options on the page.

Toggles

On the occasions when a list is guaranteed to be short (2-5 items), consider displaying it as a toggle. This shows all options and only takes one click to select.

HQ has a custom toggle widget that uses Knockout Components and is conceptually the same as a single-select dropdown. See select_toggle.js for full documentation. There's also a SelectToggle widget for use with Django forms, defined in hqwebapp's widgets.py.

A few places in HQ use the same look and feel as the select toggle widget but have unique implementations. They generally use bootstrap's button groups and also often include our own .btn-group-separated class, which separates the buttons so they're easier to read and they wrap better.

Button group

<div id='button-group-example'>
  <select-toggle params="options: ['peaceful', 'easy', 'feeling']"></select-toggle>
</div>

Select2

For most lists, select2 is the way to go. It adds behavior to a normal <select> element. It supports either hard-coded static options or dynamic options fetched via ajax. It can support free text options, acting like an autocomplete, or can restrict users to a specific list of options. Beyond these major options, select2 supports many more specific features and styling options; see the full documentation for details.

We instantiate select2 in a number of different ways: directly in javascript, via several different knockout bindings, and by using CSS classes that certain javascript modules search for. We also have a number of sets of select2 options for common behaviors like validating email addresses, displaying form questions. Before you add a new custom set of select2 options, look around and ask around for other parts of HQ that have similar behavior.

Select2








<div id="example-select2">
  <!-- Manual javascript initialization -->
  <select class="form-control basic">
    <option>one</option>
    <option>two</option>
    <option>three</option>
  </select>
  <br><br>

  <!-- .hqwebapp-select2: Doesn't need javascript initialization, depends on hqwebapp/js/bootstrap3/widgets module -->
  <select class="form-control hqwebapp-select2">
    <option>uno</option>
    <option>dos</option>
    <option>tres</option>
  </select>
  <br><br>

  <!-- .hqwebapp-select2: Also handles multiselects -->
  <select multiple class="form-control hqwebapp-select2">
    <option>moja</option>
    <option>mbili</option>
    <option>tatu</option>
  </select>
  <br><br>

  <!-- Dynamic knockout binding: initialized by the koApplyBindings call below, options are provided by knockout -->
  <div class="ko-model-dynamic">
    <select class="form-control" data-bind="select2: letters, value: value"></select>
  </div>
  <br>

  <!--
    Static binding: initialized by the koApplyBindings call below, options are pulled from HTML.
    This is useful for select2s that have non-varying options but don't work with .hqwebapp-select2
    because they're inside of a knockout-controlled UI, so they aren't guaranteed to exist on page render.
  -->
  <div class="ko-model-static">
    <select class="form-control" data-bind="staticSelect2: {}">
      <option>un</option>
      <option>deux</option>
      <option>trois</option>
    </select>
  </div>
</div>

<script>
  $(function () {
    $("#example-select2 .basic").select2();

    $("#example-select2 .ko-model-dynamic").koApplyBindings(function () {
      return {
        letters: ['eins', 'zwei', 'drei'],
        value: ko.observable('eins'),
      };
    });

    $("#example-select2 .ko-model-static").koApplyBindings();
  });
</script>

Multiselects

This is a custom widget we built. It can be useful in situations where the user is adding/removing items from a list and wants to be able to see both the included and excluded items.

It's more complicated than a dropdown and takes up much more space. Be cautious adding it to new pages - be sure that the visual weight and potential learning curve is worthwhile for the workflow you're creating.

Optional Properties

In addition to the optional title properties, the following properties can be useful in situations where more control is needed.

disableModifyAllActions - defaults to false, useful when the preferred workflow is to disable the ability to select and remove all items at once
willSelectAllListener - provides an opportunity to execute code prior to all items being selected

Multiselect

<select id='example-multiselect'>
  <option>alpha</option>
  <option>beta</option>
  <option>gamma</option>
  <option>delta</option>
  <option>epsilon</option>
</select>

<script>
  let listener = function() {
    console.log("Triggered willSelectAllListener");
  };

  $(function () {
    var multiselect_utils = hqImport('hqwebapp/js/multiselect_utils');
    multiselect_utils.createFullMultiselectWidget('example-multiselect', {
      selectableHeaderTitle: gettext("Available Letters"),
      selectedHeaderTitle: gettext("Letters Selected"),
      searchItemTitle: gettext("Search Letters..."),
      disableModifyAllActions: false,
      willSelectAllListener: listener,
    });
  });
</script>

Other selection interactions

There are several other interactions used on HQ to select items from a list. In general, these should be avoided for the following reasons:

Standard HTML select elements
There's nothing inherently wrong with these, but since so many dropdowns use select2, select elements without this styling create visual inconsistency. It should typically be trivial to turn a standard select element into a select2. Standard dropdowns do interfere with usability when their list of options gets long - with 15+ items, it becomes difficult to find and select an item - so long lists in particular should be switched to select2.
Lists of checkboxes
Like standard HTML select elements, there's nothing inherently wrong with these, but because we don't use them often, they're bad for a consistent user experience.
At.js
At.js is a library for mentioning people in comments. It's the basis for easy references in form builder. Form builder also uses it for autocomplete behavior (form builder doesn't have select2 available). A couple of places in HQ also use it for autocomplete behavior, which is not ideal. It was initially introduced in HQ because it was visually lighter weight than the legacy version of select2. However, as we move away from legacy select2, there isn't a need to use it as an autocomplete.

HQ typically doesn't use checkboxes to select options from a list, but we do frequently use single checkboxes for true/false values, usually whether a setting should be on or off.

HTML markup for checkboxes

Checkboxes get wrapped in a .checkbox div that aligns them properly with their label. Checkboxes that appear in larger forms can use the form's standard .control-label as a label but also often have additional help text that appears in the checkbox's own label element.

Bootstrap documentation

Checkbox alone

<div class="checkbox">
  <label>
    <input type="checkbox" />
    This checkbox is all by itself
  </label>
</div>

Checkbox in form

Height (cm)
Smoking status
Heart Rate (bpm)
<div class="form-horizontal">
  <div class="form-group">
    <div class="col-sm-3 control-label">
      Height (cm)
    </div>
    <div class="col-sm-9">
      <input type="text" class="form-control" />
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-3 control-label">
      Smoking status
    </div>
    <div class="col-sm-9">
      <div class="checkbox">
        <label>
          <input type="checkbox" />
          Has patient smoked on a habitual basis in the past 5 years?
        </label>
      </div>
    </div>
  </div>
  <div class="form-group">
    <div class="col-sm-3 control-label">
      Heart Rate (bpm)
    </div>
    <div class="col-sm-9">
      <input type="text" class="form-control" />
    </div>
  </div>
</div>

Crispy forms and checkboxes

Checkboxes in crispy forms can be challenging to style. Checkboxes are the default representation for a BooleanField, but they're displayed with the label to the right of the checkbox, instead of using a .control-label on the left as for other fields.

To get a label to appear on the left, checkboxes are sometimes displayed as a single-element B3MultiField, a custom field used in HQ to display multiple fields under the same left-hand label. However, this causes the checkbox itself to be mis-aligned with the other form fields.

When using a checkbox in a crispy form, if possible, use the default crispy.Field display, but place the checkbox after a related input, so it looks like an additional option on that field.

Crispy form with checkboxes

Basic Information

Located in corehq.apps.styleguide.example_forms

from django import forms
from django.utils.translation import gettext_lazy, gettext as _
from crispy_forms import layout as crispy
from crispy_forms import bootstrap as twbscrispy
from corehq.apps.hqwebapp import crispy as hqcrispy


class CheckboxesForm(forms.Form):
    send_email = forms.BooleanField(
        label=gettext_lazy("This checkbox is badly aligned"),
        required=False,
    )
    recipient = forms.CharField(
        label=gettext_lazy("Email recipient"),
    )
    send_to_self = forms.BooleanField(
        label=gettext_lazy("Also send to myself"),
        required=False,
    )

    def __init__(self, *args, **kwargs):
        super(CheckboxesForm, self).__init__(*args, **kwargs)
        self.helper = hqcrispy.HQFormHelper()

        self.helper.form_method = 'POST'
        self.helper.form_action = '#'

        self.helper.layout = crispy.Layout(
            crispy.Fieldset(
                _("Basic Information"),
                hqcrispy.B3MultiField(
                    _("Send email when complete"),
                    "send_email",
                ),
                crispy.Field('recipient'),
                crispy.Field('send_to_self'),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Save"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-default",
                ),
            ),
        )
    

When reaching for a modal, first consider if you can communicate this message in another way.

Modals are disruptive, confusing, poorly accessbile, blocking the user’s interaction, hard to escape, used as a junk drawer, frustrating on small screens, and add to congitive load. Consider non-modal dialogs on empy screen space, go inline, expand elements or use a new page. If you must use a modal, make sure it is easy to close, single purpose, short, and accessible.

There are lots of alternatives to modals. Most of the time, if you need to confirm an action, material design suggests instead offering an option to undo. This still gives a user an option to reverse an action, but it does not interrupt their flow and increase their congitive load. Read more on https://modalzmodalzmodalz.com/

Bootstrap documentation

Basic modal

<button class="btn btn-primary" data-toggle="modal" data-target="#style-guide-modal">
  Open Modal
</button>

<div class="modal fade" id="style-guide-modal">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button type="button" class="close" data-dismiss="modal"><span aria-hidden="true">&times;</span>
          <span class="sr-only">Close</span></button>
        <h4 class="modal-title">Modal Title</h4>
      </div>
      <div class="modal-body">
        Modal Body
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default" data-dismiss="modal">
          Cancel
        </button>
        <button type="submit" class="btn btn-primary">
          Save
        </button>
      </div>
    </div>
  </div>
</div>

Most UIs that display a list of user-created data will grow to the point that they should paginate their data.

HQ has a custom pagination component that uses Knockout Components. See pagination.js for full documentation.

For small, simple UIs, you can leave the "Showing X to Y of Z entries" text and dropdown off by adding inlinePageListOnly: true to the pagination element's params.

For a real ajax-based example, see the web users page's HTML widget, goToPage javascript, and python pagination view.

Pagination

<div id="pagination-example">
  <ul class="list-group" data-bind="foreach: items">
    <li class="list-group-item" data-bind="text: $data"></li>
  </ul>
  <pagination data-apply-bindings="false"
              params="goToPage: goToPage,
                      slug: 'style-guide',
                      perPage: perPage,
                      onLoad: onPaginationLoad,
                      totalItems: totalItems"></pagination>
</div>

<script>
  $(function () {
    var paginationExample = function () {
      var self = {};

      self.items = ko.observableArray();
      self.perPage = ko.observable();

      // Most of the time the widget will only deal with a page of items at a time
      // and goToPage will be an ajax call that will fetch some items and possibly a totalItems value
      self.allItems = _.map(_.range(23), function (i) { return "Item #" + (i + 1); });
      self.totalItems = ko.observable(self.allItems.length);
      self.goToPage = function (page) {
        self.items(self.allItems.slice(self.perPage() * (page - 1), self.perPage() * page));
      };

      // Initialize with first page of data
      self.onPaginationLoad = function () {
        self.goToPage(1);
      };

      return self;
    };

    $("#pagination-example").koApplyBindings(paginationExample());
  });
</script>

Large sets of data should usually support search.

HQ has a custom searching and filtering component that uses Knockout Components. See search_box.js for full documentation.

The immediate parameter controls whether the widget searches on every key press or only when the user clicks the search button or presses enter.

Data sets that uses search are typically large enough to also need pagination. In these cases, the search box implementation can piggyback on the pagination code; searching just sets a query value and sends the user back to the first page. The web users search box is an example of this.

For an example of client-side only search, see the data corrections HTML widget and javascript implementation .

Searching and Filtering

Instant saving can make for a better user experience.

HQ has a custom component for inline editing, based on Knockout Components. This component is visually lighter weight than a standard text box, and it upates the value immediately via an ajax call.

Because most of HQ does not use instant saving, don't use this component near or inside of another form. It works well when it's the only style of saving on the page, or when it's clearly separated from other forms.

This component has a number of options that affect the save action and the look and feel; see inline_edit.js for full documentation.

For a real example, see the case import page's HTML widget and javascript implementation . A few app manager pages use the template tag inline_edit_trans to provide an inline edit widget that handles multiple languages.

Inline Editing

<div id="inline-edit-example">
  <inline-edit data-apply-bindings="false" params="
        value: text,
        url: url,
        rows: 1,
        placeholder: gettext('Click here to add a comment'),
        errorMessage: gettext('Error updating comment. Please try again.'),
    "></inline-edit>
</div>

<script>
  $(function () {
    var inlineEditExample = function () {
      var self = {};

      self.text = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';

      // This should be a real url, either hard-coded in the django template or registered with the
      // registerurl template tag and then feteched here using initial_page_data.js's reverse.
      self.url = '';

      return self;
    };

    $("#inline-edit-example").koApplyBindings(inlineEditExample());
  });
</script>

Add help icons that users can click for contextual help.

HQ has JavaScript logic to initialize tooltip-based contextual help. Using help icons requires the Bootstrap javascript library and also hq.helpers.js. The tooltip uses Bootstrap's popover widget. It supports a variety of options, which can be set using data- attributes. The most commonly used in HQ are:

  • title
  • content
  • placement - specify where the popover should appear, relative to the icon (top, etc.)
  • html - true if the popover contains HTML markup

Help

<div id="help-example">
  <button class="btn btn-primary">Do it!</button>
  <span class="hq-help-template"
        data-title="Do what, exactly?"
        data-content="
          Put 1 teaspon baking soda in a cup. Pleace this into the volcano crater.
          Mix vinegar, dish soap, a few drops of water, and a few drops of red food coloring in a second cup.
          Quickly pour liquid into the backing soda cup in the crater.
        "
        data-placement="right">
  </span> 
</div>


<script>
  $(function () {
    // No initialization is necessary, but hqwebapp/js/bootstrap3/hq.helpers must be included on the page
  });
</script>

Add simple user feedback widget to any page.

HQ has a custom feedback component that uses Knockout Components. See feedback.js for full documentation.

Feedback

<div id="feedback-example">
  <feedback data-apply-bindings="false"
            params="featureName: featureName,
                    url: url"></feedback>
</div>


<script>
  $(function () {
    var feedbackExample = function () {
      var self = {};

      self.featureName = 'My New Feature';

      // This should be a real url, either hard-coded in the django template or registered with the
      // registerurl template tag and then fetched here using initial_page_data.js's reverse.
      //
      // If url is left blank, it will use the submit_feedback view which requires
      // a login and domain and sends feedback to settings.FEEDBACK_EMAIL
      self.url = '';

      return self;
    };

    $("#feedback-example").koApplyBindings(feedbackExample());
  });
</script>