Skip to main content

Forms & UI

Overview

SpeedPy ships with SpeedPy UI, a small Tailwind component layer for Django templates. Use these classes before reaching for long utility strings in page templates.

The live component catalogue is available inside the boilerplate at:

  • /speedpyui-preview/ — component examples and copyable snippets
  • /speedpyui-preview/FormView — a Django FormView rendered with crispy forms
  • /demo/products/ — a complete CRUD demo built with Django generic views

SpeedPy UI classes live in static/mainapp/input.css inside Tailwind's @layer components. Tailwind compiles them into static/mainapp/styles.css.

Crispy Forms

SpeedPy uses django-crispy-forms with the crispy-tailwind template pack.

CRISPY_TEMPLATE_PACK = "tailwind"
CRISPY_ALLOWED_TEMPLATE_PACKS = "tailwind"

Create forms with a FormHelper and keep form_tag = False when the surrounding template owns the <form> tag:

from crispy_forms.helper import FormHelper
from crispy_forms.layout import Div, Field, Layout
from crispy_tailwind.layout import Submit


class ProductForm(forms.ModelForm):
def __init__(self, *args, **kwargs):
submit_label = kwargs.pop("submit_label", "Save")
super().__init__(*args, **kwargs)

self.helper = FormHelper()
self.helper.form_tag = False
self.helper.layout = Layout(
Field("name"),
Div(
Div(Field("sku"), css_class="md:col-span-6"),
Div(Field("category"), css_class="md:col-span-6"),
css_class="grid gap-5 md:grid-cols-12",
),
Field("description"),
Submit("submit", submit_label, css_class="btn btn-contained btn-primary"),
)

Render the form in a template with:

{% load crispy_forms_tags %}

<form method="post">
{% csrf_token %}
{% crispy form %}
</form>

Use /speedpyui-preview/FormView as the reference when you want to confirm how CharField, select boxes, radio buttons, textareas, and submit buttons render.

Component Classes

Buttons

Buttons use one base class plus variant, color, and optional size classes:

<button class="btn btn-contained btn-primary">Save</button>
<a href="/demo/products/" class="btn btn-outlined btn-primary">View products</a>
<button class="btn btn-text btn-error btn-sm">Delete</button>
  • Variants: .btn-contained, .btn-outlined, .btn-text
  • Colors: .btn-primary, .btn-secondary, .btn-success, .btn-info, .btn-warning, .btn-error, .btn-inherit
  • Sizes: .btn-sm, .btn-md, .btn-lg

Typography and Page Layout

Use shared typography and wrappers for regular pages:

<main class="section">
<div class="page-container">
<div class="page-header">
<p class="eyebrow">Demo app</p>
<h1 class="h1">Products</h1>
<p class="lead">A CRUD example built with Django generic views.</p>
</div>
</div>
</main>
  • Typography: .h1, .h2, .h3, .h4, .h5, .eyebrow, .lead
  • Layout: .section, .section-paper, .page-container, .page-header, .section-header
  • Top-level page actions: .page-header-actions, .page-header-main, .page-header-buttons

Page-level action buttons, such as Add, Create, Generate, Edit, and Delete, should sit on the right side of the header on desktop:

<div class="page-header-actions">
<div class="page-header-main">
<p class="eyebrow">Demo app</p>
<h1 class="h1">Products</h1>
<p class="lead">Manage demo products.</p>
</div>
<div class="page-header-buttons">
<a href="/demo/products/new/" class="btn btn-contained btn-primary">Add product</a>
</div>
</div>

Cards, Lists, Tables, and Status

Use these classes for common surfaces and data display:

<div class="card">
<div class="card-header">
<h2 class="h3">Product information</h2>
</div>
<div class="card-body">
<span class="badge badge-success">Active</span>
</div>
</div>
  • Cards: .card, .card-header, .card-body, .card-footer
  • Tables: .table, .table-hover, .table-striped, .table-sm
  • Lists: .list-group, .list-group-item
  • Badges: .badge, .badge-lg, .badge-primary, .badge-secondary, .badge-success, .badge-info, .badge-warning, .badge-error
  • Alerts: .alert, .alert-primary, .alert-secondary, .alert-success, .alert-info, .alert-warning, .alert-error, .alert-danger, .alert-light, .alert-neutral

Keep catalogue examples easy to scan: document one component per demo row with one matching code snippet.

Low-JS Components

These primitives are CSS-first and work well in Django templates without adding client-side behavior:

<hr class="divider">

<span class="chip chip-primary">
<span class="chip-avatar">A</span>
Assigned
</span>

<nav class="breadcrumbs" aria-label="Breadcrumb">
<ol class="breadcrumb-list">
<li class="breadcrumb-item"><a class="breadcrumb-link" href="/">Dashboard</a></li>
<li class="breadcrumb-item"><span class="breadcrumb-current" aria-current="page">Members</span></li>
</ol>
</nav>
  • Dividers: .divider, .divider-text, .divider-vertical
  • Paper surfaces: .paper, .paper-outlined, .paper-padded, .paper-elevation-0, .paper-elevation-1, .paper-elevation-3, .paper-elevation-8, .paper-elevation-16, .paper-elevation-24
  • Chips: .chip, .chip-sm, .chip-primary, .chip-secondary, .chip-success, .chip-info, .chip-warning, .chip-error, .chip-outlined, .chip-avatar, .chip-remove
  • Breadcrumbs: .breadcrumbs, .breadcrumb-list, .breadcrumb-item, .breadcrumb-link, .breadcrumb-current
  • Button groups: .btn-group, .btn-group-vertical; compose existing .btn classes inside
  • Skeletons: .skeleton, .skeleton-text, .skeleton-text-lg, .skeleton-circular, .skeleton-rectangular
  • Linear progress: .progress, .progress-sm, .progress-lg, .progress-xl, .progress-bar, .progress-primary, .progress-secondary, .progress-success, .progress-info, .progress-warning, .progress-error
  • Timeline: .timeline, .timeline-item, .timeline-marker, .timeline-marker-success, .timeline-marker-info, .timeline-marker-warning, .timeline-marker-error, .timeline-content, .timeline-title, .timeline-meta, .timeline-body
  • Stepper: .stepper, .step, .step-marker, .step-body, .step-label, .step-description, .step-completed, .step-active, .step-link
  • Accordion: .accordion, .accordion-item, .accordion-header, .accordion-body; use native <details> and <summary> unless custom behavior is explicitly required

Examples for each primitive live in /speedpyui-preview/.

Forms and Inputs

Crispy forms emit the recommended input classes automatically. Use the classes directly only when writing custom form markup:

  • Text inputs: .input-outlined, .input-outlined-sm, .input-outlined-lg
  • Textareas: .textarea-outlined
  • Selects: .select-outlined
  • Choices: .checkbox, .radio
  • Switches: .switch, .switch-track, .switch-thumb
  • Field layout: .form-field, .input-label, .input-helper, .input-error, .input-error-text

Use the shared navigation classes instead of repeating long link utilities:

  • Top nav: .top-nav, .top-nav-inner, .top-nav-brand, .top-nav-logo, .top-nav-title, .top-nav-actions, .top-nav-menu, .top-nav-list, .top-nav-item, .top-nav-link, .top-nav-icon-button, .top-nav-user-button, .top-nav-auth-link, .top-nav-dropdown, .top-nav-dropdown-header, .top-nav-dropdown-link
  • Sidebar: .sidebar, .sidebar-brand, .sidebar-brand-text, .sidebar-section-label, .sidebar-nav, .sidebar-link, .sidebar-link-active, .sidebar-link-icon, .sidebar-divider, .sidebar-select, .sidebar-dropdown, .sidebar-dropdown-item, .sidebar-dropdown-item-active
  • Account pages: .account-shell, .account-nav, .account-nav-link, .account-nav-link-active

Pagination

List pages should use numbered, elided pagination with a result summary:

<div class="pagination">
<p class="pagination-summary">Showing 1 to 10 of 300 results</p>
<nav class="pagination-list" aria-label="Pagination">
<a class="pagination-link pagination-link-disabled" aria-disabled="true">Previous</a>
<a class="pagination-link pagination-link-active">1</a>
<a class="pagination-link" href="?page=2">2</a>
<span class="pagination-ellipsis">...</span>
<a class="pagination-link" href="?page=30">30</a>
<a class="pagination-link" href="?page=2">Next</a>
</nav>
</div>

Use .pagination, .pagination-summary, .pagination-list, .pagination-link, .pagination-link-active, .pagination-link-disabled, and .pagination-ellipsis.

CRUD Demo Pattern

Full-page demos that teach boilerplate users should live in demoapp, not mainapp. Keep them conventional and easy to copy:

  • demoapp/models.py for the model
  • demoapp/forms.py for the crispy form
  • demoapp/views.py for generic class-based views
  • demoapp/urls.py for demo routes
  • templates/demoapp/ for templates

The Product demo at /demo/products/ is the canonical CRUD example. It includes:

  • ListView with filters, stats, a table, row actions, and pagination
  • CreateView and UpdateView using the same crispy ModelForm
  • DetailView with summary cards and right-side Edit/Delete actions
  • DeleteView with an explicit confirmation page

Do not seed demo rows in migrations. If a demo needs starter data, expose an explicit POST action and only show the button when the table is empty, like the Product demo's "Generate demo products" action.

Dark Mode

Tailwind dark mode uses the class strategy:

module.exports = {
darkMode: "class",
}

Most SpeedPy UI colors are token-backed, so templates usually do not need paired dark: utilities for surfaces, borders, or body text. Prefer token classes such as bg-background, bg-background-paper, text-fg, text-fg-secondary, and border-divider.

Frontend Interactivity

Alpine.js is available for lightweight template interactivity. Floating UI is available for tooltip, popover, and dropdown positioning when needed.