HTML Style Guide
Our standard for formatting HTML.
On this page
Overview
The guide offered here is a living document and by no means complete. The intention is to standardize and improve the readability of our HTML, especially given an environment where we are making use of more attribute-based frontend libraries like HTMX and Alpine.js.
Tools for Consistency
We are still working on finding a linter to raise formatting issues during pull requests. In the meantime,
we request that you make sure your git commit hooks are up-to-date. If you haven't done this in a while,
please run git-hooks/install.sh
to install the latest commit hooks, including tools to ensure
committed HTML is formatted optimally.
1. Two-Space Indentation
Use two-space indentation for nested content and multi-line attributes. This indentation rule
applies to any nested elements, including Django template if
/else
tags, for
loops, etc.
<div class="row mt-4"> {% for report in reports %} <div class="col-md-6 col-lg-6 col-xl-3"> <div id="{{ report.slug }}" class="card text-center report-panel mb-3" data-slug="{{ report.slug }}" > {% if report.is_active %} <div class="fs-5">{{ report.description }}</div> {% else %} <div class="fs-2"> {% blocktrans %} Not Active {% endblocktrans %} </div> {% endif %} </div> </div> {% endfor %} </div>
2. Block vs Inline, Text, and Translations
Elements that are block elements should start on their own line. Text within block elements should be indented on a new line as a paragraph within that element if there is more than one line of text.
Inline elements, like a
, span
, code
, etc. can be inline with
text and other inline elements.
<div class="row mt-4"> <h1>Short titles: one line ok</h1> <h2> However, longer titles (like a sentence) should be indented. </h2> <p> This is paragraph text within a block element. It should always follow standard indentation nested within its parent element. If you have a <a href="https://dimagi.com">link</a> within the paragraph, that's fine to leave inline with text, as well as <code>code</code> tags or <span data-somedata="foo">span</span> tags, etc. <a class="btn-link" href="http://dimagi.com" target="_blank" >Long links</a>, even though they are inline elements, can also behave more like block elements within the paragraph, to reduce the overall line length of that text. </p> <p> Short paragraph text should also be on its own line within a <code>p</code> parent. </p> <p> {% blocktrans %} Translated text within a paragraph tag should always be indented like this {% endblocktrans %} </p> <h3> {% trans "Short titles are fine" %} </h3> <p> If you have <img alt="{% trans "Translated text within attributes" %}">, then use <code>trans</code> instead of <code>blocktrans</code>. </p> <p> And if you have lists.... </p> <ul> <li> <p> The Indentation structure should look like this. </p> </li> </ul> </div>
3. Attribute Order and Line Breaks
-
Standard Attributes: Place primary attributes (e.g.,
id
,class
,type
) first on the element, followed by a line break if additional attributes exist or if the line exceeds 115 characters when all attributes are inline.Django<div id="column1" class="editable-column-container" hx-get="/api/data" hx-trigger="click" x-data="{ isEditing: false, isSubmitting: false, }" > ... </div>
-
HTMX Attributes (
hx-
,hq-hx-
): Place HTMX attributes on their own line immediately after standard attributes. If there are multiplehx-
attributes, place each on a separate line. -
Alpine.js Attributes: Place Alpine.js attributes (
x-
,:…
attributes) afterhx-
attributes, each on a separate line.
4. In-Attribute JavaScript-Like Formatting for Alpine.js
For complex JavaScript-like values, such as x-data
, follow this indentation:
-
Start the attribute on a new line.
-
Indent JSON or JavaScript-like values by 2 spaces.
-
Align closing brackets with the opening attribute line.
-
As with JavaScript objects, the last element should have a trailing comma.
<div id="column1" class="editable-column-container" hx-get="/api/data" hx-trigger="click" x-data="{ isEditing: false, isSubmitting: false, }" > ... </div>
5. In-Attribute JSON for hx-vals
For hx-vals, JSON should be formatted similarly to point 4, with these additional JSON formatting rules:
-
Use double quotes around JSON keys and values.
-
Do not include trailing commas for the final key-value pair in the object.
-
For Django Template tags within JSON (e.g.,
{{ }}
for variables or{% %}
for logic), ensure template syntax doesn’t break JSON formatting, paying attention to surrounding quotes.
<div hx-post="{{ request.path_info }}" hq-hx-action="edit_cell_value" hx-vals='{ "recordId": {{ record.id }}, "column": "{{ column.accessor }}" }' hx-disabled-elt="find button" hx-swap="outerHTML" > ... </div>
6. Attribute Wrapping and Line Length
Limit line length to improve readability, ideally keeping lines below 115 characters maximum, with 80 characters being the best line length for readability.
-
If the line is less than 80 characters with any attributes (including Alpine and HTMX attributes) then it’s fine to keep all the attributes on a single line.
Django<div class="mb-3" x-show="isEditing"> ... </div>
-
If the line is more than 80 characters and has HTMX/Alpine/
data-
attributes, then the HTMX/Alpine/data-
attributes should all be on new lines.Django<button id="my-offcanvas-button" class="btn btn-outline-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#configureColumnsOffcanvas" hx-post="{% url 'my_new_view' %}" hq-hx-action="test_button" x-show="isReadyToSubmit" > ... </button>
-
If non-HTMX/Alpine/
data-
attributes exceed 115 characters on one line, wrap attributes so that each line does not exceed 115 characters. Each line may include multiple attributes.Django<button id="my-offcanvas-button" class="btn btn-outline-primary mb-3 pe-3" type="button" aria-label="{% trans 'Close' %}" tabindex="1" > ... </button>
7. Self-Closing Elements
For self-closing elements, such as <input />
:
-
Ensure that the trailing slash (
/
) is included to clearly indicate the element is self-closing. -
Follow the same attribute ordering and indentation rules as outlined for other elements.
<input class="form-control" type="text" name="newValue" x-model="cellValue" />
If it fits on a single line, remember to still include the trailing slash. Please still follow the rules from point 6 when it comes to attribute wrapping.
<input class="form-control" type="text" name="newValue" />
8. Best Practices for Code Readability with HTMX and Alpine
-
Consistency: Always follow this attribute order and indentation style throughout the codebase.
-
Comments: Use Django template comments (
{# ... #}
) to explain complex Alpine or HTMX interactions when necessary. -
Minimize Inline JavaScript: Keep JavaScript logic within the component or JavaScript files rather than inline, except for simple Alpine data bindings or event handlers.
Example Structure
<button id="submit-button" class="btn btn-primary" hx-post="/api/submit" hx-swap="innerHTML" x-data="{ isDisabled: false, onClick() { this.isDisabled = true; }, }" :disabled="isDisabled" > Submit </button>