This guide will be deprecated soon!

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

Molecules connect into organisms: even larger building blocks, the kind that make up a substantial portion of the design of a new feature or significant update to an existing feature.

Forms on CommCare HQ

Forms in HQ are a mix of bespoke HTML and Crispy Forms. Different parts of HQ use these two different approaches; both are supported.

HTML forms

HQ uses styles provided by Bootstrap 3 Forms, with the majority of forms using the form-horizontal style.

Notes on the example below:

  • Forms also need to include a {% csrf_token %} tag to protect against CSRF attacks. HQ will reject forms that do not contain this token.
  • The sets of grid classes (col-sm-*, etc.) can be replaced by {% css_field_class %}, {% css_label_class %}, and {% css_action_class %}, which will fill in HQ's standard form proportions.
  • The dropdown here (and throughout this section) should use a select2, as discussed in selections.
  • The textarea uses the vertical-resize class to allow for long input. Inputs that accept XPath expressions are especially likely to have very long input. The text area does not support horizontal resizing, which can allow the user to expand a textarea so that it overlaps with other elements or otherwise disrupts the page's layout.
  • The autocomplete attribute controls the browser's form autofill. Most forms in HQ are unique to HQ and should set turn off autocomplete to prevent unexpected automatic input. Exceptions would be forms that include information like a user's name and address.
  • This example does not show translations, but all user-facing text should be translated.

Simple HTML form

Basic Information
Cancel
<form action="#" class="form-horizontal" method="post">
  <fieldset>
    <legend>Basic Information</legend>
    <div class="form-group">
      <label for="id_first_name" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        First Name
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <input type="text" name="first_name" class="form-control" id="id_first_name" autocomplete="off" />
      </div>
    </div>
    <div class="form-group">
      <label for="id_favorite_color" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        Favorite Color
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <select name="favorite_color" class="form-control" id="id_favorite_color">
          <option value="red">Red</option>
          <option value="green">Green</option>
          <option value="blue">Blue</option>
        </select>
      </div>
    </div>
    <div class="form-group">
      <label for="id_hopes" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        Hopes and Dreams
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <textarea name="hopes" class="form-control vertical-resize" id="id_hopes"></textarea>
      </div>
    </div>
  </fieldset>
  <div class="form-actions">
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 col-sm-offset-4 col-md-offset-4 col-lg-offset-2 controls">
      <button class="btn btn btn-primary" type="submit">Save</button>
      <a href="#" class="btn btn-default">Cancel</a>
    </div>
  </div>
</form>

Form states and error messaging

Good error messages are specific, actionable, visually near the affected input. They occur as soon as a problem is detected. They help the user figure out how to address the situation: "Sorry, this isn't supported. Try XXX." Without any cues, the user is stuck in the same frustrating situation.

Errors in forms should be displayed near the relevant input using the .has-error class. Note that .has-error is applied to the input's parent container, while any error message should have be marked with .help-block. There are .has-warning and .has-success classes similar to .has-error, though we don't use them often.

For form-level or page-level errors, use django's standard messages framework: messages.error, etc. In javascript, our alert_user module provides similar functionality.

Form with errors

Basic Information
This is an error message.
Cancel
<form action="#" class="form-horizontal" method="post">
  <fieldset>
    <legend>Basic Information</legend>
    <div class="form-group">
      <label for="id_normal_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        Normal Input
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <input type="text" name="normal_input" class="form-control" id="id_normal_input" />
      </div>
    </div>
    <div class="form-group">
      <label for="id_hint_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        Hint Text
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <input type="text" name="hint_input" class="form-control" placeholder='This is a hint' id="id_hint_input" />
      </div>
    </div>
    <div class="form-group">
      <label for="id_disabled_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label text-muted">
        Disabled Input
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <input type="text" name="disabled_input" class="form-control" disabled id="id_disabled_input" />
      </div>
    </div>
    <div class="form-group has-error">
      <label for="id_error_input" class="col-xs-12 col-sm-4 col-md-4 col-lg-2 control-label">
        Error Input
      </label>
      <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 controls">
        <input type="text" name="error_input" class="form-control" id="id_error_input" />
        <span class='help-block'>This is an error message.</span>
      </div>
    </div>
  </fieldset>
  <div class="form-actions">
    <div class="col-xs-12 col-sm-8 col-md-8 col-lg-6 col-sm-offset-4 col-md-offset-4 col-lg-offset-2 controls">
      <button class="btn btn btn-primary" type="submit">Save</button>
      <a href="#" class="btn btn-default">Cancel</a>
    </div>
  </div>
</form>

Glossary of HTML form elements and classes

Fieldset
A grouping of form fields.
<fieldset>
    ...
</fieldset>
Fieldset Legend
A title that describes the group of fields succinctly. Always present as the first tag inside a <fieldset />
<fieldset>
    <legend>Basic Information</legend>
    ...
</fieldset>
Form Group
An item that provides one bit of functionality or collects one subset of data for the form. It contains controls and its control-label and is present inside the <fieldset> after the <legend>. Sometimes the control group might have one label for a set of related fields (multi field) or a modifiable table of objects.
<fieldset>
    <legend>Basic Information</legend>
    <div class="form-group">
        <label for="emailInput" class="col-lg-2 control-label">Email</label>
        <div class="col-lg-10 controls">
            <input id="emailInput" ...>
        </div>
    </div>
    ...
</fieldset>
Control Label
A descriptive label that is visually aligned to the left of its controls.
<div class="form-group">
    <label for="emailInput" class="col-lg-2 control-label">Email</label>
    <div class="col-lg-10 controls">
        <input id="emailInput" ...>
    </div>
</div>
Note that the for attribute refers to the id attribute of the input. Clicking on the label will focus on the input that has the id specified in for.
Controls

A functional unit / input in the form.

Form Actions
A group of buttons that affect the processing or navigation for the entire form and is present before the closing </form> tag.
<div class="form-group form-actions">
    ...
</div>
Submit Action

It is always the first button specified in Form Actions and appears as the left-most button. It has the following visual appearance, using the btn-primary class:

It submits the form and triggers its processing.

Secondary Action

In Form Actions, it is the button with the following visual appearance, using the btn-default class:

Typically, this is the cancel button. It does some other action that causes you to navigate away from the form, but not save.

Crispy Forms

Example with annotated source

Crispy Forms generates HTML for forms based on python form definitions. Using it contributes to consistency in design and reduces boilerplate HTML writing. Crispy does not control the form's logic, processing, validation, or anything having to do with form data. It can be used specify hooks for the display logic.

Notes on the example below:

  • The form_method and form_action are the keys to how and where the form is submitted.
  • self.helper controls the label and field widths. Best practice is to inherit from a class like HQFormHelper or HQModalFormHelper (both defined in hqwebapp.crispy) to get the standard form proportions. If necessary, custom classes can be set using self.helper.label_class and self.helper.field_class. The form's .form-horizontal is also automatically set by HQFormHelper and similar classes, but it can be overridden with self.helper.form_class
  • Resize this window to see the form responsiveness in action. HQFormHelper uses .col-xs-12.col-sm-4.col-md-4.col-lg-2 for labels and .col-xs-12.col-sm-8.col-md-8.col-lg-6 for fields. On a laptop or monitor, you likely see 1:4 or 1:3 proportions, but if you shrink the browser window enough, you'll see the label and field eventually stack on top of each other. See bootstrap grid documentation for a more thorough understanding of the responsive grid system.
  • All layout objects accept the attribute css_class to specify custom css-classes to the field, like so:
    hqcrispy.LinkButton(
        _("Cancel"),
        '#',
        css_class="btn btn-default",
    )
  • Other attributes, for example a data-bind="", can be added like so:
    crispy.Field(
        'first_name',
        data_bind="value: firstName",
    )
    All underscores (_) in the attributes in Python turn into hyphens (-) in HTML, e.g., data_bind="foo" becomes data-bind="foo".

Basic Crispy Form

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 BasicCrispyForm(forms.Form):
    first_name = forms.CharField(
        label=gettext_lazy("First Name"),
    )
    favorite_color = forms.ChoiceField(
        label=gettext_lazy("Pick a Favorite Color"),
        choices=(
            ('red', gettext_lazy("Red")),
            ('green', gettext_lazy("Green")),
            ('blue', gettext_lazy("Blue")),
            ('purple', gettext_lazy("Purple")),
        ),
    )

    def __init__(self, *args, **kwargs):
        super(BasicCrispyForm, 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"),
                crispy.Field('first_name'),
                crispy.Field('favorite_color'),
            ),
            hqcrispy.FormActions(
                twbscrispy.StrictButton(
                    _("Save"),
                    type="submit",
                    css_class="btn btn-primary",
                ),
                hqcrispy.LinkButton(
                    _("Cancel"),
                    '#',
                    css_class="btn btn-default",
                ),
            ),
        )
    

Tables

Keep tables scannable and think about how big they might get.

When adding a table, first consider the nature the information you're displaying. Tables are best suited to tabular data, so if that's not what you're working with, consider other design options.

To increase scannability:

  • Remove uncessary design
  • Don't stretch tables
  • Align headings with data
  • Align to the decimal point
  • Use whitespace
  • Keep small screens in mind, consider oblique headers (headers at a 45-degree angle)
  • In languages that read left to right, left-align text and right-align numbers

Recommended reading: https://alistapart.com/article/web-typography-tables

Most of our tables use hand-crafted markup based on Bootstrap's styles. Some areas of CommCare, particularly reporting, use DataTables and are tightly integrated with the python code that generates the data. The remainder of this section is primarily relevant to Bootstrap tables.

Bootstrap documentation

Basic Tables

To control column spacing, use the .col-(xs|sm|mg|lg)-[0-9] classes provided by Bootstrap's grid system .

Basic table

Case Type Name Owner Status
patient Arundhati worker1 open
patient Karan worker4 open
patient Salman worker1 open
patient Aravind worker4 closed
patient Katherine worker3 closed
<table class="table table-striped table-hover">
  <thead>
  <tr>
    <th class="col-sm-2">Case Type</th>
    <th class="col-sm-4">Name</th>
    <th class="col-sm-4">Owner</th>
    <th class="col-sm-2">Status</th>
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>patient</td>
    <td>Arundhati</td>
    <td>worker1</td>
    <td>open</td>
  </tr>
  <tr>
    <td>patient</td>
    <td>Karan</td>
    <td>worker4</td>
    <td>open</td>
  </tr>
  <tr>
    <td>patient</td>
    <td>Salman</td>
    <td>worker1</td>
    <td>open</td>
  </tr>
  <tr>
    <td>patient</td>
    <td>Aravind</td>
    <td>worker4</td>
    <td>closed</td>
  </tr>
  <tr>
    <td>patient</td>
    <td>Katherine</td>
    <td>worker3</td>
    <td>closed</td>
  </tr>
  </tbody>
</table>

Scaling Tables

Most tables of user-created data will, at least for some projects, grow to a point where they should support a summary, pagination, and possibly searching. Your table doesn't necessarily need all of these, but consider how users are likely to use your table and manipulate the data in it.

Table with total, pagination, and search

5 of 23 cases
Case Type Name Owner Status Action
patient Arundhati worker1 open
patient Karan worker4 open
patient Salman worker1 open
patient Aravind worker4 closed
patient Katherine worker3 closed
Showing 1 to 5 of 23 entries
<div class="form-inline pull-right">
  <div class="input-group">
    <input type="text" class="form-control" placeholder="Filter Cases" />
    <span class="input-group-btn">
            <button type="button" class="btn btn-default">
                <i class="fa fa-search"></i>
            </button>
        </span>
  </div>
</div>
<strong>5 of 23 cases</strong>

<table class="table table-striped table-hover">
  <thead>
    <tr>
      <th class="col-sm-2">
        <i class="fa fa-unsorted text-muted"></i>
        Case Type
      </th>
      <th class="col-sm-3">
        <i class="fa fa-unsorted text-muted"></i>
        Name
      </th>
      <th class="col-sm-3">
        <i class="fa fa-unsorted text-muted"></i>
        Owner
      </th>
      <th class="col-sm-2">
        <i class="fa fa-sort-amount-asc"></i>
        Status
      </th>
      <th class="col-sm-2">Action</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>patient</td>
      <td>Arundhati</td>
      <td>worker1</td>
      <td>open</td>
      <td><button class="btn btn-danger">Archive</button></td>
    </tr>
    <tr>
      <td>patient</td>
      <td>Karan</td>
      <td>worker4</td>
      <td>open</td>
      <td><button class="btn btn-danger">Archive</button></td>
    </tr>
    <tr>
      <td>patient</td>
      <td>Salman</td>
      <td>worker1</td>
      <td>open</td>
      <td><button class="btn btn-danger">Archive</button></td>
    </tr>
    <tr>
      <td>patient</td>
      <td>Aravind</td>
      <td>worker4</td>
      <td>closed</td>
      <td><button class="btn btn-danger">Unarchive</button></td>
    </tr>
    <tr>
      <td>patient</td>
      <td>Katherine</td>
      <td>worker3</td>
      <td>closed</td>
      <td><button class="btn btn-danger">Unarchive</button></td>
    </tr>
  </tbody>
</table>

<div class="row">
  <div class="col-sm-5">
    <div class="form-inline pagination-text">
      <span>Showing 1 to 5 of 23 entries</span>
      <select class="form-control">
        <option value="5" selected>5 per page</option>
        <option value="25">25 per page</option>
        <option value="50">50 per page</option>
        <option value="100">100 per page</option>
      </select>
    </div>
  </div>
  <div class="col-sm-7 text-right">
    <ul class="pagination">
      <li><a href="#">&raquo;</a></li>
      <li class="active"><a href="#">1</a></li>
      <li><a href="#">2</a></li>
      <li><a href="#">3</a></li>
      <li><a href="#">4</a></li>
      <li><a href="#">5</a></li>
      <li><a href="#">&raquo;</a></li>
    </ul>
  </div>
</div>