Skip to content

Django forms

Django renders forms without classes. Wrap them in <form class="ss-c-form"> and the form module styles that markup as it comes: field wrappers, labels, <div class="helptext">, <ul class="errorlist">, <ul class="errorlist nonfield">, aria-invalid widgets, required_css_class markers, and the fieldset Django uses for radio and checkbox groups. The selectors carry no specificity, so a class on a widget (attrs={"class": "ss-c-input"}) always wins. The markup below is Django 6 output, pasted unchanged. For hand-written templates the same look is available as classes (last example).

Unbound form

{{ form }} with required_css_class = "required": help text, a RadioSelect, a CheckboxSelectMultiple, a textarea and a BooleanField.

Your full name as it appears on invoices.
Topic:
Channels:
Leave empty to opt out.
HTML
<form class="ss-c-form" method="post" action="#" novalidate aria-label="Contact (unbound)" style="max-width: 560px">
            <div class="required">
                <label for="id_clean_name" class="required">Name:</label>
            <div class="helptext" id="id_clean_name_helptext">Your full name as it appears on invoices.</div>
            <input type="text" name="name" maxlength="80" required aria-describedby="id_clean_name_helptext" id="id_clean_name">
            </div>
              <div class="required">
                <label for="id_clean_email" class="required">Email:</label>
            <input type="email" name="email" maxlength="320" required id="id_clean_email">
            </div>
              <div class="required">
                <fieldset>
              <legend class="required">Topic:</legend>
            <div id="id_clean_topic"><div>
                <label for="id_clean_topic_0"><input type="radio" name="topic" value="q" required id="id_clean_topic_0">
             Question</label>
            </div><div>
                <label for="id_clean_topic_1"><input type="radio" name="topic" value="b" required id="id_clean_topic_1">
             Bug report</label>
            </div><div>
                <label for="id_clean_topic_2"><input type="radio" name="topic" value="f" required id="id_clean_topic_2">
             Feedback</label>
            </div>
            </div></fieldset>
            </div>
              <div>
                <fieldset aria-describedby="id_clean_channels_helptext">
              <legend>Channels:</legend>
            <div class="helptext" id="id_clean_channels_helptext">Leave empty to opt out.</div>
            <div id="id_clean_channels"><div>
                <label for="id_clean_channels_0"><input type="checkbox" name="channels" value="mail" id="id_clean_channels_0">
             E-mail</label>
            </div><div>
                <label for="id_clean_channels_1"><input type="checkbox" name="channels" value="sms" id="id_clean_channels_1">
             SMS</label>
            </div>
            </div></fieldset>
            </div>
              <div class="required">
                <label for="id_clean_message" class="required">Message:</label>
            <textarea name="message" cols="40" rows="4" required id="id_clean_message">
</textarea>
            </div>
              <div>
                <label for="id_clean_subscribe">Subscribe to the newsletter:</label>
            <input type="checkbox" name="subscribe" id="id_clean_subscribe">
            </div>
            <div class="ss-c-form__actions">
                <button type="submit" class="ss-c-button">Send</button>
            </div>
        </form>

Bound form with errors

non_field_errors render as a boxed summary at the top; field errors sit above their widget, which Django marks aria-invalid and links with aria-describedby.

  • Name and e-mail do not match an existing account.
Your full name as it appears on invoices.
  • This field is required.
  • Enter a valid email address.
Topic:
Channels:
Leave empty to opt out.
HTML
<form class="ss-c-form" method="post" action="#" novalidate aria-label="Contact (with errors)" style="max-width: 560px">
            <ul class="errorlist nonfield"><li>Name and e-mail do not match an existing account.</li></ul>
              <div class="error required">
                <label for="id_bound_name" class="required">Name:</label>
            <div class="helptext" id="id_bound_name_helptext">Your full name as it appears on invoices.</div>
            <ul class="errorlist" id="id_bound_name_error"><li>This field is required.</li></ul>
            <input type="text" name="name" maxlength="80" required aria-invalid="true" aria-describedby="id_bound_name_helptext id_bound_name_error" id="id_bound_name">
            </div>
              <div class="error required">
                <label for="id_bound_email" class="required">Email:</label>
            <ul class="errorlist" id="id_bound_email_error"><li>Enter a valid email address.</li></ul>
            <input type="email" name="email" value="not-an-email" maxlength="320" required aria-invalid="true" aria-describedby="id_bound_email_error" id="id_bound_email">
            </div>
              <div class="required">
                <fieldset>
              <legend class="required">Topic:</legend>
            <div id="id_bound_topic"><div>
                <label for="id_bound_topic_0"><input type="radio" name="topic" value="q" required id="id_bound_topic_0" checked>
             Question</label>
            </div><div>
                <label for="id_bound_topic_1"><input type="radio" name="topic" value="b" required id="id_bound_topic_1">
             Bug report</label>
            </div><div>
                <label for="id_bound_topic_2"><input type="radio" name="topic" value="f" required id="id_bound_topic_2">
             Feedback</label>
            </div>
            </div></fieldset>
            </div>
              <div>
                <fieldset aria-describedby="id_bound_channels_helptext">
              <legend>Channels:</legend>
            <div class="helptext" id="id_bound_channels_helptext">Leave empty to opt out.</div>
            <div id="id_bound_channels"><div>
                <label for="id_bound_channels_0"><input type="checkbox" name="channels" value="mail" id="id_bound_channels_0">
             E-mail</label>
            </div><div>
                <label for="id_bound_channels_1"><input type="checkbox" name="channels" value="sms" id="id_bound_channels_1">
             SMS</label>
            </div>
            </div></fieldset>
            </div>
              <div class="required">
                <label for="id_bound_message" class="required">Message:</label>
            <textarea name="message" cols="40" rows="4" required id="id_bound_message">
Hi</textarea>
            </div>
              <div>
                <label for="id_bound_subscribe">Subscribe to the newsletter:</label>
            <input type="checkbox" name="subscribe" id="id_bound_subscribe">
            </div>
            <div class="ss-c-form__actions">
                <button type="submit" class="ss-c-button">Send</button>
            </div>
        </form>

form.as_p

as_p puts each error list before its paragraph and the help text in a span.

  • This field is required.

Shown in the list.

  • Ensure this value is greater than or equal to 0.

HTML
<form class="ss-c-form" method="post" action="#" novalidate aria-label="Item (as_p)" style="max-width: 560px">
            <ul class="errorlist" id="id_p_title_error"><li>This field is required.</li></ul>
              <p>
                <label for="id_p_title">Title:</label>
                <input type="text" name="title" required aria-invalid="true" aria-describedby="id_p_title_helptext id_p_title_error" id="id_p_title">
                  <span class="helptext" id="id_p_title_helptext">Shown in the list.</span>
              </p>
              <ul class="errorlist" id="id_p_count_error"><li>Ensure this value is greater than or equal to 0.</li></ul>
              <p>
                <label for="id_p_count">Count:</label>
                <input type="number" name="count" value="-1" min="0" required aria-invalid="true" aria-describedby="id_p_count_error" id="id_p_count">
              </p>
            <div class="ss-c-form__actions">
                <button type="submit" class="ss-c-button">Send</button>
            </div>
        </form>

Formset

The management form is four hidden inputs; each form goes in its own <fieldset>: {{ formset.management_form }}{% for form in formset %}<fieldset>…{{ form }}</fieldset>{% endfor %}. Wrap them in ss-c-form__formset for tighter spacing. Formset-level errors use errorlist nonform and look like nonfield.

Item 1
Item 2
  • This field is required.
  • Ensure this value is greater than or equal to 1.
HTML
<form class="ss-c-form" method="post" action="#" novalidate aria-label="Order items (formset)" style="max-width: 560px">
            <div class="ss-c-form__formset">

                <input type="hidden" name="items-TOTAL_FORMS" value="2" id="id_fs_items-TOTAL_FORMS"><input type="hidden" name="items-INITIAL_FORMS" value="0" id="id_fs_items-INITIAL_FORMS"><input type="hidden" name="items-MIN_NUM_FORMS" value="1" id="id_fs_items-MIN_NUM_FORMS"><input type="hidden" name="items-MAX_NUM_FORMS" value="1000" id="id_fs_items-MAX_NUM_FORMS">
                <fieldset>
                <legend>Item 1</legend>
                <div>
                    <label for="id_fs_items-0-product">Product:</label>
                <input type="text" name="items-0-product" value="Sensor" id="id_fs_items-0-product">
                </div>
                  <div>
                    <label for="id_fs_items-0-quantity">Quantity:</label>
                <input type="number" name="items-0-quantity" value="2" min="1" id="id_fs_items-0-quantity">
                </div>
                  <div>
                    <label for="id_fs_items-0-DELETE">Delete:</label>
                <input type="checkbox" name="items-0-DELETE" id="id_fs_items-0-DELETE">
                </div>
                </fieldset>
                <fieldset>
                <legend>Item 2</legend>
                <div>
                    <label for="id_fs_items-1-product">Product:</label>
                <ul class="errorlist" id="id_fs_items-1-product_error"><li>This field is required.</li></ul>
                <input type="text" name="items-1-product" aria-invalid="true" aria-describedby="id_fs_items-1-product_error" id="id_fs_items-1-product">
                </div>
                  <div>
                    <label for="id_fs_items-1-quantity">Quantity:</label>
                <ul class="errorlist" id="id_fs_items-1-quantity_error"><li>Ensure this value is greater than or equal to 1.</li></ul>
                <input type="number" name="items-1-quantity" value="0" min="1" aria-invalid="true" aria-describedby="id_fs_items-1-quantity_error" id="id_fs_items-1-quantity">
                </div>
                  <div>
                    <label for="id_fs_items-1-DELETE">Delete:</label>
                <input type="checkbox" name="items-1-DELETE" id="id_fs_items-1-DELETE">
                </div>
                </fieldset>

            </div>
            <div class="ss-c-form__actions">
                <button type="submit" class="ss-c-button">Send</button>
            </div>
        </form>

Explicit classes

For hand-written templates: ss-c-form__field, __label, __help, __errors, __errors--nonfield, __fieldset/__legend, __choices, __field--inline and __field--invalid.

  • Your session expired. Check the fields and send again.
We only use it to send the receipt.
  • Enter a valid email address.
Delivery
Time slot
HTML
<form class="ss-c-form" method="post" action="#" novalidate aria-label="Order (explicit classes)" style="max-width: 560px">
            <ul class="ss-c-form__errors ss-c-form__errors--nonfield">
                <li>Your session expired. Check the fields and send again.</li>
            </ul>
            <div class="ss-c-form__field ss-c-form__field--invalid">
                <label class="ss-c-form__label ss-c-form__required" for="ex-email">E-mail</label>
                <div class="ss-c-form__help" id="ex-email_helptext">We only use it to send the receipt.</div>
                <ul class="ss-c-form__errors" id="ex-email_error"><li>Enter a valid email address.</li></ul>
                <input class="ss-c-input" type="email" id="ex-email" name="email" value="anna@" aria-invalid="true" aria-describedby="ex-email_helptext ex-email_error">
            </div>
            <fieldset class="ss-c-form__fieldset">
                <legend class="ss-c-form__legend">Delivery</legend>
                <div class="ss-c-form__field">
                    <label class="ss-c-form__label" for="ex-street">Street</label>
                    <input class="ss-c-input" type="text" id="ex-street" name="street">
                </div>
                <div class="ss-c-form__field">
                    <span class="ss-c-form__label" id="ex-slot-label">Time slot</span>
                    <div class="ss-c-form__choices" role="radiogroup" aria-labelledby="ex-slot-label">
                        <label><input class="ss-c-radio" type="radio" name="slot" checked> Morning</label>
                        <label><input class="ss-c-radio" type="radio" name="slot"> Afternoon</label>
                    </div>
                </div>
                <div class="ss-c-form__field ss-c-form__field--inline">
                    <label for="ex-gift">Gift wrap</label>
                    <input class="ss-c-checkbox" type="checkbox" id="ex-gift" name="gift">
                </div>
            </fieldset>
            <div class="ss-c-form__actions">
                <button type="submit" class="ss-c-button">Order</button>
                <button type="button" class="ss-c-button">Cancel</button>
            </div>
        </form>