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 DjangoFormViewrendered 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.btnclasses 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
Navigation
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.pyfor the modeldemoapp/forms.pyfor the crispy formdemoapp/views.pyfor generic class-based viewsdemoapp/urls.pyfor demo routestemplates/demoapp/for templates
The Product demo at /demo/products/ is the canonical CRUD example. It includes:
ListViewwith filters, stats, a table, row actions, and paginationCreateViewandUpdateViewusing the same crispyModelFormDetailViewwith summary cards and right-side Edit/Delete actionsDeleteViewwith 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.